No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Olivier Cortès 61deb04736 Fix DELTA traceability line fit
Honor traceability.meta_font_scale, compact full SHA pairs onto one physical line, and fail validation on clipping or wrapping.

Assisted-by: Félin Capiteux <felin@lodlp.net>
2026-09-28 10:17:01 +02:00
profiles Fix DELTA traceability line fit 2026-09-28 10:17:01 +02:00
tests Fix DELTA traceability line fit 2026-09-28 10:17:01 +02:00
tools Initial public release 2026-09-27 18:52:10 +02:00
.gitignore Initial public release 2026-09-27 18:52:10 +02:00
.gitlab-ci.yml Initial public release 2026-09-27 18:52:10 +02:00
AGENTS.md Fix DELTA traceability line fit 2026-09-28 10:17:01 +02:00
asami-md2pdf Fix DELTA traceability line fit 2026-09-28 10:17:01 +02:00
LICENSE Initial public release 2026-09-27 18:52:10 +02:00
README.md Fix DELTA traceability line fit 2026-09-28 10:17:01 +02:00
requirements.txt Initial public release 2026-09-27 18:52:10 +02:00

Specification Report Generator

Specification Report Generator is the PDF renderer used by Asami's specification-approval workflow to turn a human-readable Markdown validation report into a stable, printable PDF.

The repository contains:

  • the asami-md2pdf renderer;
  • the normal PDF layout profile;
  • twenty prequalified paper-compaction profiles stored as individual YAML files;
  • reproducible synthetic tests for the qualified renderer and paper-selection table;
  • a maintainer calibration tool for rebuilding that table when layout parameters change.

The renderer is intentionally narrow. It does not decide what a specification means, write the report, estimate semantic reading complexity, or silently shorten content to save paper. Those decisions belong upstream. Its job is to render an already-written report faithfully, make it easy to reference during human review, expose measurable pagination information, and validate important PDF invariants.

What the generated report looks like

For the specification-validation report format, the renderer adds presentation-only addressing and metadata without changing the Markdown source:

  • the first # heading becomes TITLE – DATETIME and is centered;
  • the reading-time labels are centered and joined on one line with •;
  • sections and subsections are numbered automatically;
  • prose paragraphs are numbered continuously in the left margin;
  • paragraph body text is justified;
  • inline monospace text receives a light gray background, 0.3em padding, and rounded corners;
  • code blocks keep their own block presentation;
  • the footer is printed on every page with the document name on the left two-thirds and edition datetime plus page/total on the right third;
  • final revision-traceability lines are compacted to the FULL form (one SHA-256) or the DELTA form (before SHA-256 → current SHA-256), always on one physical line.

The section and paragraph numbers are generated at render time. They are navigation aids, not report content, and therefore do not contribute to the report's editorial word count or reading-time budget.

Qualified environment

The initial public import corresponds to the renderer qualified on 27 September 2026:

Component Qualified value
Renderer asami-md2pdf 1.1.1
Python used for initial qualification 3.13.5
WeasyPrint 62.3
Markdown 3.7
PyYAML 6.0.2
Poppler 25.03.0
Locale in the supplied profiles fr-FR
Timezone Europe/Paris

The renderer source imported here initially had SHA-256:

9f84a2e015782b483a7b21bb4d3ee4f7962924e136f827acde95aaffe4315532

The normal DSL initially had SHA-256:

66da15bc48b2d1825d9beb23d3e2cc52d1dfda5ccb57db41819be78ca2202c5f

Other versions may render successfully, but they are not automatically covered by the visual qualification above. A change of renderer, layout engine, primary fonts, or layout rules requires renewed qualification.

Requirements

Python packages:

Markdown==3.7
PyYAML==6.0.2
WeasyPrint==62.3

System tools used by the renderer and validation path:

  • pdftotext from Poppler, including -bbox support;
  • pdffonts from Poppler;
  • a working WeasyPrint native stack;
  • Noto Color Emoji for the traceability pictograms not covered by Ubuntu Sans.

The supplied profiles use the repository-relative directory fonts/ as a portable default. You can instead pass any directory explicitly with --font-root /path/to/fonts. The directory must contain these exact filenames:

UbuntuSans[wdth,wght].ttf
UbuntuSans-Italic[wdth,wght].ttf
UbuntuSansMono[wght].ttf
UbuntuSansMono-Italic[wght].ttf

The font binaries are deliberately not stored in this repository. The renderer must be given the canonical font files from the environment or from the normative source used by the surrounding project.

For a Python virtual environment outside Debian packaging, requirements.txt contains the qualified Python versions. Poppler and native WeasyPrint dependencies still have to be installed by the operating system.

Installation on your computer

Install the executable somewhere on your PATH, for example a user-local bin directory:

install -m 0755 asami-md2pdf ~/.local/bin/asami-md2pdf

Keep the YAML files from profiles/ together with your checkout or copy them to a location of your choice. Pass the exact profile path with --dsl.

Basic usage

Render a Markdown report with the normal profile:

asami-md2pdf \
  --dsl profiles/spec-validation-pdf-v2.yaml \
  --input report.md \
  --output report.pdf

Useful optional arguments:

  • --document-name NAME — override the document name derived from the first Markdown # heading;
  • --font-root PATH — override typography.font_root, mainly for testing or bootstrap;
  • --generated-at ISO8601 — force the edition datetime. If omitted, the renderer uses the current time in the DSL timezone.

A fixed --generated-at is useful for reproducible tests:

asami-md2pdf \
  --dsl profiles/spec-validation-pdf-v2.yaml \
  --input report.md \
  --output report.pdf \
  --generated-at '2026-09-27T17:20:00+02:00'

The renderer writes one JSON receipt to standard output after successful generation. The receipt includes, among other fields:

  • renderer and WeasyPrint versions;
  • selected profile;
  • output page count;
  • last-page fill fraction and percentage;
  • number of body words detected on the last page;
  • whether the normal result is a paper-compaction candidate;
  • PDF byte size and SHA-256;
  • font/footer/traceability validation results when required by the DSL.

Example fields:

{
  "profile": "normal",
  "pages": 3,
  "last_page_fill_percent": 43.8,
  "paper_compaction_candidate": true,
  "program_version": "1.1.1",
  "weasyprint_version": "62.3",
  "footer_verified": true,
  "fonts_verified": ["UbuntuSans", "UbuntuSansMono"],
  "traceability_line_fit_verified": true
}

Markdown contract for Asami specification reports

The renderer can render ordinary Markdown, but several report-specific transformations recognize the canonical French validation-report labels.

Title

The first Markdown H1 is the document name:

# Rapport de validation globale — exemple

The renderer appends the edition datetime for display:

Rapport de validation globale — exemple – 27/09/2026 17:20

The timestamp is render-only metadata and is not inserted back into the Markdown.

Reading-time subtitle

The canonical report uses:

**Specification set soumis à revue humaine avant approbation globale**<br>
**Temps de lecture cible : 16 minutes**<br>
**Minimum lisible estimé : 12 minutes**

The renderer centers the subtitle and joins the two reading-time labels as:

Temps de lecture cible : 16 minutes • Minimum lisible estimé : 12 minutes

It also accepts the historical two-paragraph form for the reading-time labels. The labels themselves are deliberately recognized in French because this renderer originated in the Asami validation workflow; changing those literals requires a renderer change and requalification.

Section and paragraph addressing

Markdown headings ## through ###### receive hierarchical numbering. Prose paragraphs receive a continuous number in the left margin.

Those numbers are CSS-generated and are not part of the Markdown source. This is important for the specification workflow: reading-time budgets are based on editorial prose, not on navigation markers introduced by the renderer.

Traceability block

The canonical heading is:

## Traçabilité des révisions soumises

Within that section, a card entry such as:

- **Master — example**<br>
  Carte `1234567890123456789`<br>
  SHA-256 `0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef`

is rendered with the metadata on one compact line:

🎴 1234567890123456789 ⋅ 🔒 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

The pictograms are semantic navigation aids requested by the report contract, not decorative images.

The DELTA form uses the same card entry, with a full before SHA-256, an arrow, and a full current SHA-256. NEW → <current-sha256> is also accepted for a newly introduced card. The renderer keeps card IDs and hashes in Ubuntu Sans Mono, removes list indentation from this traceability block, and applies traceability.meta_font_scale only to the compact metadata line.

When validation.require_traceability_line_fit is enabled, generation fails unless every rendered traceability metadata item can be extracted in full from one physical PDF line. This turns clipping or wrapping of before → current into a terminal renderer error instead of a visually silent defect.

Reading-time budgeting

asami-md2pdf does not estimate reading complexity and does not count words to decide how much the author should write.

In the Asami specification workflow, the agent first estimates:

  • a semantic minimum readable time below which explanatory prose would become cryptic;
  • a recommended reading time with a comfort margin.

Only after that semantic decision is made is the target duration converted into an approximate prose budget using the workflow convention of 300 words per minute.

Render-only section numbers, subsection numbers, paragraph numbers, footer content, and generated title datetime do not count toward that editorial budget.

Paper optimization

Goal

A report is printed duplex. If the normal PDF ends on a lightly used odd page, that page can consume an entire additional sheet. The paper optimization therefore tries to remove exactly that final odd page without changing the Markdown or deleting content.

Compaction only changes predefined layout parameters such as body size, heading size, line height, and page margins. The footer and addressing contract remain stable.

When compaction is allowed

Keep the normal profile when any of these is true:

  • the document has an even number of pages;
  • the document has only one page;
  • an odd document's final page is filled to more than 50%.

A normal result becomes a compaction candidate only when:

page count >= 3
AND page count is odd
AND last-page fill <= 50%

The renderer reports this as paper_compaction_candidate.

How last-page fill is measured

The renderer runs:

pdftotext -bbox

and examines the bounding boxes of body words on the final page. Footer words are excluded by the page-margin boundary. The fill fraction is based on the vertical position of the last body word relative to the usable body height:

(max body y - top margin) / usable body height

Because text occupies discrete line boxes, an exact value of 50.0% is not necessarily representable. The reference calibration used the densest observable fixtures still below the 50% boundary, approximately 48.6–48.7% in that campaign.

Prequalified direct-selection table

The nominal workflow must not try profiles one after another. Render once with normal, inspect the receipt, then directly select the prequalified profile:

Normal result Approximate boundary case Direct profile Required final result
3 pages ~2.5 pages compact-13 2 pages
5 pages ~4.5 pages compact-8 4 pages
7 pages ~6.5 pages compact-7 6 pages
9 pages ~8.5 pages compact-5 8 pages
11 pages ~10.5 pages compact-4 10 pages

Example: a normal three-page PDF with a final-page fill of 42% is rendered once more with:

asami-md2pdf \
  --dsl profiles/spec-validation-pdf-v2-compact-13.yaml \
  --input report.md \
  --output report.pdf

The final receipt must then report two pages and must still satisfy the font/footer/traceability validations.

Why there are twenty compact profiles

Short documents need proportionally more compression to remove half a page than long documents. A coarse profile family caused several different page counts to collapse onto the same aggressive setting.

The current family therefore contains twenty small, monotonic steps. This lets a long document use only a mild reduction while a short document can use a stronger profile when necessary.

The profiles are stored bare and individually in profiles/ so they can be reviewed and modified one at a time. A tarball may be produced for a downstream workflow, but that archive is a derived distribution artifact and must never become the editable source of truth.

Outside the calibrated range

Do not guess.

If a candidate has an odd page count not present in the table, or the selected profile fails to reach the previous even page count:

  1. keep the best already-validated rendering;
  2. report that the case is outside the calibrated contract;
  3. recalibrate the profile family;
  4. update the normative workflow and this repository together.

Do not invent per-report font sizes, margins, CSS coefficients, or a one-off YAML profile.

Recalibrating the paper profiles

tools/calibrate_profiles.py is a maintainer tool. Unlike nominal report generation, calibration intentionally probes the compact profiles in order to determine the first sufficient profile for each target page count.

Run the complete calibration on your computer with the qualified dependencies and fonts:

python3 tools/calibrate_profiles.py

Verify that it still matches the repository contract:

python3 tools/calibrate_profiles.py --check

JSON output is also available:

python3 tools/calibrate_profiles.py --format json

The calibration process:

  1. builds synthetic reports with varied paragraph lengths and technical tokens;
  2. finds, for each odd page count 3, 5, 7, 9, 11, the densest normal fixture observable at or below 50% terminal fill;
  3. tries compact-1 through compact-20 only in this maintainer calibration context;
  4. records the first profile that reaches exactly the preceding even page count;
  5. compares the resulting mapping with the qualified table when --check is used.

The synthetic fixtures deliberately contain no private specification content.

A changed mapping is not automatically accepted. It is evidence that layout behavior changed and must be reviewed, visually qualified, and reflected in the owning specification before it becomes the new nominal table.

Validation

Automated suite

On your computer:

python3 -m unittest discover -s tests -v

The suite checks:

  • the complete normal + compact-1…20 profile set exists;
  • renderer 1.1.1 runs on the qualified WeasyPrint 62.3 environment;
  • a normal synthetic report produces the expected validated receipt;
  • title datetime and reading-time presentation survive text extraction;
  • section numbering and traceability glyphs are present;
  • a full DELTA before-sha256 → current-sha256 survives on one physical PDF line;
  • each prequalified paper mapping reaches the previous even page count.

PDF-level validation

The renderer itself performs terminal checks requested by the DSL, including:

  • required Ubuntu font families appear embedded according to pdffonts;
  • footer document name, datetime, first pagination, and last pagination can be extracted;
  • traceability glyphs can be extracted;
  • every traceability metadata item fits completely on one physical PDF line when requested by the DSL;
  • last-page body fill can be measured.

These checks do not replace semantic review of the source report.

Visual qualification

A renderer/layout/font/profile change must be followed by explicit visual review of representative final PDFs. The automated suite proves structural properties, not human readability.

A practical review uses pdftoppm to render pages to images and inspects every affected page for:

  • clipping or overlap;
  • title/subtitle alignment;
  • margin paragraph numbers;
  • section hierarchy;
  • justified body text;
  • inline monospace background/padding;
  • traceability line fit;
  • footer size and alignment;
  • acceptable density in the strongest compact profile under review.

Once a renderer revision has been visually qualified, unchanged data-only generations may inherit that qualification only when the surrounding PDF policy explicitly allows it and all automatic terminal checks continue to pass.

Profile organization

The source profile set is:

profiles/
├── spec-validation-pdf-v2.yaml
├── spec-validation-pdf-v2-compact-1.yaml
├── spec-validation-pdf-v2-compact-2.yaml
├── ...
└── spec-validation-pdf-v2-compact-20.yaml

The normal profile defines the baseline. Each compact file is a complete YAML profile rather than a runtime patch, so every rendering configuration is directly inspectable and reproducible.

The compact profiles preserve stable elements such as the footer while gradually changing density-related values. Do not assume that a higher profile number is appropriate merely because more compression sounds desirable; nominal selection is by the qualified table, not by preference.

Producing a derived profile bundle

Some downstream workflows may prefer one transfer artifact. The repository sources stay bare; a bundle is generated from them when needed.

A deterministic bundle can be created with GNU tar/gzip using fixed metadata, for example:

LC_ALL=C tar --sort=name --mtime='UTC 1970-01-01' --owner=0 --group=0 --numeric-owner \
  -C profiles -cf - . | gzip -n > spec-validation-pdf-v2-profiles.tar.gz

The archive is disposable and reproducible. Edit the YAML files, never the archive.

Development on your computer

Clone the repository to any directory on your computer. No machine name, filesystem layout, SSH key path, or hosting credential is part of the project contract. Configure Git authentication using the mechanism appropriate for your own environment.

For local qualification, provide the font directory explicitly:

ASAMI_PDF_FONT_ROOT=/path/to/fonts python3 -m unittest discover -s tests -v
ASAMI_PDF_FONT_ROOT=/path/to/fonts python3 tools/calibrate_profiles.py --check

Keep host-specific paths, credentials, SSH configuration, and deployment notes outside the repository.

Repository layout

.
├── AGENTS.md
├── README.md
├── asami-md2pdf
├── profiles/
│   ├── spec-validation-pdf-v2.yaml
│   └── spec-validation-pdf-v2-compact-1.yaml … compact-20.yaml
├── requirements.txt
├── tests/
│   ├── common.py
│   ├── test_renderer.py
│   └── test_paper_profiles.py
└── tools/
    └── calibrate_profiles.py

Scope and non-goals

This project deliberately does not:

  • write or summarize a specification;
  • decide whether a specification is approved;
  • infer the minimum or recommended semantic reading time;
  • change prose to fit a paper target;
  • create decorative PDF content;
  • auto-search for the most compressed profile during ordinary generation;
  • silently adapt layout outside the versioned DSL family;
  • bundle or redistribute the normative Ubuntu font binaries.

The surrounding workflow remains responsible for source authority, semantic fidelity, report attachment, printing, and the explicit human approval gate.

License

This project is licensed under the GNU General Public License, version 3 or any later version.

SPDX identifier:

GPL-3.0-or-later

See LICENSE for the complete GNU GPL version 3 license text.