Initial iftypeset pipeline

This commit is contained in:
codex 2026-01-03 20:29:35 +00:00
commit 626779d4aa
112 changed files with 9550 additions and 0 deletions

25
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,25 @@
name: ci
on:
push:
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install deps
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
- name: Run CI
run: bash scripts/ci.sh

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.venv/
.env/
.DS_Store
/out/
/out-css/
/dist/
/build/

41
README.md Normal file
View file

@ -0,0 +1,41 @@
# iftypeset (pubstyle) — publication-quality typesetting pipeline
This project is a **thin, deterministic runtime** for turning Markdown into highquality HTML/PDF using:
- **A machinereadable rule registry** (Chicago / Bringhurst pointers; paraphrased rules only)
- **Typeset profiles** (`spec/profiles/*.yaml`) that map typographic intent → render tokens
- **Postrender QA gates** (`spec/quality_gates.yaml`) that fail builds when layout degrades
It is designed to be embedded into constrained workers (e.g. Forgejo PDF export with `--network=none`) and also run as a standalone CLI.
**Status:** working spec + seeded rule registry. See `STATUS.md`.
## Constraints (non-negotiable)
- **No bulk OCR/transcription** of Chicago/Bringhurst into this repo (copyright).
- Rule records are **paraphrases only**, backed by **pointers** (e.g. `CMOS18 §X.Y pNNN (scan pMMM)`).
- Chicago OCR (when needed) must be **ephemeral** (extract just enough to locate pointers; do not store page text).
## Quickstart (current)
From `ai-workspace/iftypeset/`:
- (Optional) Install deps into a venv: `python3 -m venv .venv && . .venv/bin/activate && python -m pip install -r requirements.txt`
- Validate spec + rebuild indexes: `PYTHONPATH=src python3 -m iftypeset.cli validate-spec --spec spec --build-indexes`
- Lint Markdown: `PYTHONPATH=src python3 -m iftypeset.cli lint --input fixtures/sample.md --out out --profile web_pdf`
- Render HTML + CSS: `PYTHONPATH=src python3 -m iftypeset.cli render-html --input fixtures/sample.md --out out --profile web_pdf`
- Render PDF (if an engine is installed): `PYTHONPATH=src python3 -m iftypeset.cli render-pdf --input fixtures/sample.md --out out --profile web_pdf`
- Run QA gates (HTML fallback if no PDF): `PYTHONPATH=src python3 -m iftypeset.cli qa --out out --profile web_pdf`
- Coverage report: `PYTHONPATH=src python3 -m iftypeset.cli report --spec spec --out out`
- Run self-check tests: `python3 -m unittest discover -s tests -p 'test_*.py'`
## PDF renderers
`render-pdf` will use the first available engine in this order:
- `playwright` (Python module)
- `chromium` / `chromium-browser` / `google-chrome`
- `wkhtmltopdf`
- `weasyprint` (Python module)
If none are installed, the command exits with a clear message but still leaves HTML artifacts for QA.

78
STATUS.md Normal file
View file

@ -0,0 +1,78 @@
# iftypeset status (pubstyle)
**Updated:** 2026-01-03
**Project root:** `/root/ai-workspace/iftypeset/`
## What exists (working today)
- **Spec + schema:** `spec/schema/rule.schema.json`, `spec/manifest.yaml`
- **Profiles:** `spec/profiles/*.yaml` (`web_pdf`, `print_pdf`, `dense_tech`, `memo`, `slide_deck`)
- **Post-render QA gates:** `spec/quality_gates.yaml`
- **Rule registry (seeded):** `spec/rules/**.ndjson`
- **Indexes (derived):** `spec/indexes/*.json` (rebuildable)
- **CLI:** `validate-spec`, `report`, `lint`, `render-html`, `render-pdf`, `qa`, `emit-css`
- **Ephemeral extraction helpers:** `tools/` (Chicago OCR is grep-only, temp files deleted)
- **Forgejo integration note:** `forgejo/README.md`
- **Fixtures + tests:** `fixtures/` and `tests/`
- **CI script:** `scripts/ci.sh` (validate-spec, report, unit tests)
## Rule corpus snapshot
From `out/coverage-report.json`:
- **Total rules:** 307
- **By category:** citations 61, numbers 62, punctuation 55, layout 46, headings 32, tables 23, typography 15, links 5, accessibility 4, code 4
- **By enforcement:** manual 186, typeset 62, lint 46, postrender 13
- **By severity:** must 28, should 263, warn 16
## Current rule batches
- `spec/rules/accessibility/v1_accessibility_001.ndjson` (4)
- `spec/rules/citations/v1_citations_001.ndjson` (16)
- `spec/rules/citations/v1_citations_002.ndjson` (45)
- `spec/rules/code/v1_code_001.ndjson` (4)
- `spec/rules/headings/v1_headings_001.ndjson` (12)
- `spec/rules/headings/v1_headings_002.ndjson` (20)
- `spec/rules/layout/v1_layout_001.ndjson` (12)
- `spec/rules/layout/v1_layout_002.ndjson` (30)
- `spec/rules/layout/v1_layout_003.ndjson` (4)
- `spec/rules/links/v1_links_001.ndjson` (5)
- `spec/rules/numbers/v1_numbers_001.ndjson` (12)
- `spec/rules/numbers/v1_numbers_002.ndjson` (50)
- `spec/rules/punctuation/v1_punctuation_001.ndjson` (15)
- `spec/rules/punctuation/v1_punctuation_002.ndjson` (40)
- `spec/rules/tables/v1_tables_001.ndjson` (8)
- `spec/rules/tables/v1_tables_002.ndjson` (15)
- `spec/rules/typography/v1_typography_001.ndjson` (8)
- `spec/rules/typography/v1_typography_002.ndjson` (7)
## How to validate and inspect
- Validate spec + rebuild indexes:
- `PYTHONPATH=src python3 -m iftypeset.cli validate-spec --spec spec --build-indexes`
- Lint:
- `PYTHONPATH=src python3 -m iftypeset.cli lint --input fixtures/sample.md --out out --profile web_pdf`
- Render HTML + CSS:
- `PYTHONPATH=src python3 -m iftypeset.cli render-html --input fixtures/sample.md --out out --profile web_pdf`
- Render PDF (if renderer installed):
- `PYTHONPATH=src python3 -m iftypeset.cli render-pdf --input fixtures/sample.md --out out --profile web_pdf`
- Run QA gates (HTML fallback if no PDF):
- `PYTHONPATH=src python3 -m iftypeset.cli qa --out out --profile web_pdf`
- Coverage report:
- `PYTHONPATH=src python3 -m iftypeset.cli report --spec spec --out out --build-indexes`
- Emit CSS for a profile:
- `PYTHONPATH=src python3 -m iftypeset.cli emit-css --spec spec --profile web_pdf --out out-css`
- Run unit tests:
- `python3 -m unittest discover -s tests -p 'test_*.py'`
## Key constraints (dont drift)
- **No bulk OCR/transcription** of books into repo. Rules must be paraphrased and pointer-backed.
- `source_refs` must be **pointers**, not quotes; include `(scan pN)` only as a single page hint.
- Chicago extraction may use OCR **ephemerally** only to locate pointers; do not persist OCR output.
## Next work (highest leverage)
- Add new batches for: `figures`, `frontmatter`, `backmatter`, `abbreviations`, `i18n`, and expand `accessibility`.
- Grow post-render QA rule coverage (widows/orphans, heading keeps, overflow) beyond the current seed set.
- Add a real PDF-layout analyzer when a stable renderer is selected (widows/orphans, overflow).

93
app/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,93 @@
# Runtime Architecture
This is a thin, deterministic runtime that:
A) ingests Markdown → normalizes a document AST → applies editorial lint (Chicago-derived)
B) applies typeset tokens/profile (Bringhurst-derived)
C) renders HTML and PDF deterministically
D) runs post-render QA gates (widows/orphans, heading keeps, overflow)
E) generates `layout-report.json` and fails builds when thresholds are exceeded
Primary reference PDFs are used for pointer-based traceability only:
* The Chicago Manual of Style (18th ed).pdf
* Robert Bringhurst The Elements of Typographic Style.pdf
No bulk transcription is performed; rules are paraphrases and cite sources only by pointer.
## Components
### 1) Registry Loader
Inputs:
* `spec/rules/**.ndjson` (Phase 2 output)
* `spec/schema/rule.schema.json`
* `spec/manifest.yaml`
* `spec/profiles/*.yaml`
* `spec/quality_gates.yaml`
Responsibilities:
* validate each rule against JSON Schema
* enforce ID uniqueness and stable sorting
* build or load indexes in `spec/indexes/*.json`
* compute coverage (implemented vs unimplemented; by enforcement)
Output (in-memory):
* `RuleStore` (rules + indexes + profile overrides + gate thresholds)
### 2) Markdown Ingest + AST Normalization
Design note: initial scaffold will ship with a minimal Markdown parser strategy and can be adapted to the existing Forgejo renderer pipeline.
Degraded mode:
* If parsing fails or structure is missing, switch to minimal node set and mark `structure_confidence: low`.
* Run the “degraded mode contract” from `spec/manifest.yaml`.
### 3) Editorial Lint Engine
* Runs `lint`-enforced rules against normalized AST.
* Emits diagnostics (`lint-report.json`, optional SARIF).
* Autofix is optional and must be deterministic.
### 4) Typeset Profile Engine
* Converts typographic intent into deterministic render inputs (CSS tokens + policies).
* Emits `render.css`, `typeset-report.json`.
### 5) Deterministic Rendering
* HTML generation must be stable (DOM order, IDs, whitespace).
* PDF generation must be deterministic given the same inputs and renderer version.
### 6) Post-render QA Analyzer
Detects:
* widows/orphans
* stranded headings (keep-with-next)
* overfull lines
* table overflow/clipping
* code overflow/clipping
* link wrap incidents
Artifacts:
* `layout-report.json` (canonical QA report)
* `qa-report.json` (gate evaluation + failures)
## Coverage Reporting and CI Guardrails
Coverage is computed from:
* total active rules
* rules with an implemented enforcement handler (lint/typeset/postrender/manual)
CI policy (from manifest):
* fail if MUST coverage drops
* fail if overall implemented coverage drops

130
app/CLI_SPEC.md Normal file
View file

@ -0,0 +1,130 @@
# CLI Specification
The CLI is designed for CI use: deterministic outputs, stable exit codes, and JSON artifacts for tooling.
## Common flags (all commands)
This is the *target* contract; v0 currently implements a subset per-command.
* `--spec <dir>`: Spec root directory (default: `spec/`).
* `--input <path>`: Markdown file or directory (where applicable).
* `--out <dir>`: Output directory (default: `out/`).
* `--profile <name>`: One of: `web_pdf`, `print_pdf`, `dense_tech`, `memo`, `slide_deck` (where applicable).
* `--strict`: Use strict thresholds in `spec/quality_gates.yaml` (QA/report).
* `--format <json|sarif|text>`: Lint output format.
* `--fail-on <must|should|warn>`: Lowest severity that fails `lint` (default: `must`).
* `--degraded-ok`: Allow degraded mode without failing (lint/render-html).
* `--version`: Print tool version and exit.
## Command: `validate-spec`
Purpose:
* Validate YAML/JSON spec files and (when present) rule NDJSON batches.
Outputs:
* `out/spec-validation.json`
Exit codes:
* `0`: ok
* `4`: config/schema error
* `5`: internal error
## Command: `report`
Purpose:
* Produce a consolidated report (coverage + indexes status).
Outputs:
* `out/coverage-report.json`
* `out/coverage-summary.md`
Exit codes:
* `0`: report built
* `2`: coverage floor violated (only when rule batches exist)
* `4`: config/schema error
* `5`: internal error
## Command: `lint`
Purpose:
* Parse Markdown into a minimal block AST (headings, paragraphs, lists, code fences, tables).
* Emit deterministic diagnostics and a manual checklist derived from the registry.
Outputs:
* `out/lint-report.json`
* `out/manual-checklist.md`
* `out/manual-checklist.json`
* `out/degraded-mode-report.json` (only when degraded mode triggers)
* `out/lint-report.sarif` (only when `--format sarif`)
* `out/fix-suggestions.json` (only when `--fix --fix-mode suggest`)
* `out/fixed/*.md` (only when `--fix --fix-mode rewrite`)
Exit codes:
* `0`: ok
* `2`: lint failed (fail-on threshold exceeded, or degraded mode without `--degraded-ok`)
* `4`: config error
## Command: `render-html`
Purpose:
* Render Markdown to deterministic HTML and CSS based on a profile.
Outputs:
* `out/render.html`
* `out/render.css`
* `out/typeset-report.json`
* `out/degraded-mode-report.json` (only when degraded mode triggers)
Exit codes:
* `0`: ok
* `2`: degraded mode without `--degraded-ok`
* `4`: config error
## Command: `render-pdf`
Purpose:
* Render Markdown to PDF using the first available engine (playwright/chromium/wkhtmltopdf/weasyprint).
Outputs:
* `out/render.html`
* `out/render.css`
* `out/typeset-report.json`
* `out/render-log.json`
* `out/render.pdf` (only when rendering succeeds)
Exit codes:
* `0`: ok
* `3`: renderer missing or engine error (see `out/render-log.json`)
* `4`: config error
## Command: `qa`
Purpose:
* Run deterministic post-render QA checks (HTML analysis v0) and enforce numeric gates from `spec/quality_gates.yaml`.
Outputs:
* `out/layout-report.json`
* `out/qa-report.json`
Exit codes:
* `0`: gates pass
* `2`: gates fail
* `4`: config error / missing `out/render.html`

View file

@ -0,0 +1,98 @@
# iftypeset v0.1 — Demo Acceptance Pack
This doc defines what “shipready” means for `iftypeset` (pubstyle): a deterministic Markdown → HTML → PDF pipeline with enforceable quality gates and auditable artifacts.
## Goals
- Produce **consistently good-looking** outputs from plain Markdown without manual layout heroics.
- Provide **machine-verifiable QA gates** (layout/report JSON) suitable for CI.
- Keep the system **renderer-agnostic** via adapters (Chromium / WeasyPrint / Prince / Antenna House / Vivliostyle, etc.).
- Preserve legal constraints: **rules are paraphrases + pointers**, never book text.
## Definition of Done (v0.1)
“Done” means the following commands are boring and reliable:
- `lint` emits deterministic diagnostics and a manual checklist.
- `render-html` emits deterministic HTML + CSS for a profile.
- `qa` runs on HTML (always) and on PDF (when available) and fails builds when gates exceed thresholds.
- `report` shows rule coverage and doesnt regress.
## Profiles to Support in v0.1
- `web_pdf` (screen-first): tolerant, readable, strong accessibility defaults.
- `dense_tech` (specs): tighter measure targets, code/tables common, strict numbering/citations.
- `memo` (internal): conservative, low typographic complexity.
## Fixture Set (minimum)
Create and maintain fixtures that represent real “pain” documents. v0.1 should ship with at least these:
1. **Memo (short, mixed content)** — bullets + links + a couple of headings
- Stress: heading hierarchy, link wrapping, list spacing.
2. **Dense technical note** — headings, numbered sections, code blocks, small tables
- Stress: code wrapping/overflow policy, table overflow policy, numbering monotonicity.
3. **Report with many links** — long URLs/DOIs/emails, references section
- Stress: link wrap policy, footnote/reference formatting, readability.
4. **Table-heavy checklist** — 35 tables, some wide
- Stress: table overflow handling, header repeat policy (HTML), clipping detection (PDF when possible).
5. **Degraded input** — hard-wrapped paragraphs + inconsistent headings
- Stress: degraded-mode contract: unwrap/recover safely + emit degraded-mode report.
## Acceptance Gates (must pass)
### Lint gates (always on)
- No schema/spec validation failures.
- `lint-report.json` is produced and deterministic across two runs.
- `manual-checklist.md` is produced and contains only rules tagged `manual_checklist=true`.
### HTML render gates (always on)
- `render.html` and `render.css` produced deterministically.
- CSS tokens reflect chosen profile (page size, margins, font stacks, line-height).
- No external fetches in HTML (self-contained mode must embed local assets).
### QA gates (HTML fallback; PDF when available)
Minimum v0.1 gate set:
- `max_link_wrap_incidents` (catch “unbreakable” URLs / DOIs / emails).
- `max_table_overflow_incidents` (wide tables).
- `max_code_overflow_incidents` (wide code blocks).
- `max_stranded_headings` (keep-with-next heuristic).
- `max_heading_numbering_errors` (basic numbering monotonicity).
PDF-only gates (enable when a PDF engine is available):
- widows/orphans
- overfull lines (glyph boxes exceed text block)
## Multi-renderer Compatibility (design requirement)
v0.1 should treat the PDF engine as an adapter. Acceptance criteria:
- `render-pdf --engine auto`:
- selects an available engine,
- writes `render-log.json` including engine name + version,
- fails clearly if no engine available (but keeps HTML artifacts).
Renderer capability differences must be explicit in `render-log.json` and `qa-report.json` (e.g., “widow/orphan detection unavailable for HTML-only run”).
## Demo Script (for humans)
For a convincing “this is real” demo:
1. Run `lint` + show `lint-report.json` and `manual-checklist.md`.
2. Run `render-html` + open `out/render.html` (show the profile look).
3. Run `qa` + show `qa-report.json` (pass/fail, counts).
4. If PDF engine exists, show the rendered PDF and the same QA gates on PDF.
## Release Checklist (v0.1)
- [ ] At least 30 fixtures exist and are exercised in CI.
- [ ] CI fails when QA thresholds are exceeded (no “green by vibes”).
- [ ] CI fails on spec regression (coverage floors, schema validation).
- [ ] Degraded mode emits its report artifacts and never silently “fixes” content.
- [ ] Renderer adapters are documented (how `auto` chooses, how to pin).

View file

@ -0,0 +1,85 @@
# iftypeset — Competitor / Positioning Matrix (v0.1)
This is a practical market map to keep us honest about what exists today and what `iftypeset` is trying to be.
## The category were building
Not “Markdown to PDF” (thats solved), but:
**Deterministic publishing CI**: Markdown → styled output **plus enforceable QA gates** (widows/orphans/overflow/keeps/link wrap) with machine-readable reports and coverage tracking.
## High-level landscape
### A) Renderers / converters (output-first)
Great at converting formats, but usually do **not** ship “layout QA gates” as a product.
- Pandoc (+ LaTeX) / Quarto / RMarkdown
- Typst
- LaTeX toolchains
- Markdown site tools that export PDF (MkDocs, Docusaurus, GitBook, Notion exports)
### B) Paged-media engines (layout-first)
Excellent at pagination + print rules, but they dont give you a Chicago/Bringhurst rule registry or a publishing QA runtime by default.
- PrinceXML
- Antenna House Formatter
- WeasyPrint
- Vivliostyle / Paged.js
- wkhtmltopdf (HTML → PDF, limited paged-media fidelity)
### C) SaaS PDF rendering APIs
Operational convenience; QA gates are typically “your responsibility”.
- DocRaptor (Prince-powered)
- Various HTML→PDF APIs (vendor-specific)
## Feature comparison (typical, not absolute)
Legend:
- **✓**: first-class / native
- **~**: possible but not the default product shape
- **—**: not typical / not supported
| Capability | iftypeset (goal) | Pandoc/Quarto | Typst | LaTeX | Prince/AH | WeasyPrint | Vivliostyle/Paged.js |
|---|---:|---:|---:|---:|---:|---:|---:|
| Markdown → HTML | ✓ | ✓ | ~ | ~ | — | — | ✓ |
| Markdown → PDF | ✓ (via adapters) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (via headless browser) |
| Deterministic artifacts (stable HTML/IDs) | ✓ | ~ | ~ | ~ | ~ | ~ | ~ |
| Profile tokens (web_pdf/print_pdf/etc.) | ✓ | ~ | ~ | ~ | ~ | ~ | ~ |
| Rule registry w/ pointers (no quotes) | ✓ | — | — | — | — | — | — |
| Lint + autofix (editorial hygiene) | ✓ | ~ | ~ | ~ | — | — | — |
| Post-render QA gates (widows/orphans/overflow) | ✓ | — | — | — | ~ | ~ | ~ |
| Coverage reporting for implemented rules | ✓ | — | — | — | — | — | — |
| Degraded-mode handling (garbage inputs) | ✓ | ~ | ~ | ~ | — | — | — |
## Whats actually different about iftypeset
1. **Quality is a build gate**
Not “this looks nice”, but “this fails CI if link-wrap/table overflow/stranded headings exceed thresholds”.
2. **Rules are a registry (not hardcoded CSS)**
Chicago/Bringhurst become paraphrased, pointer-backed records you can audit, diff, and expand over time.
3. **Renderer-agnostic**
The PDF engine is pluggable. The “meaning” is in tokens + QA + reports, not the renderer choice.
4. **Traceability-compatible**
Machine outputs (reports/coverage) can be hashed/signed and attached to IF.TRACE receipts.
## Who pays for this (practical)
- Teams who must ship PDFs that survive scrutiny:
- **GRC / security** (SOC2/ISO evidence packs, policy docs)
- **research/publishing** (tech reports, standards commentary)
- **legal/professional services** (deliverables that must look “court-ready”)
- **vendor marketing with constraints** (docs that must be consistent across versions)
## Messaging that is honest
- “Markdown in, publication-quality out — with QA gates and receipts.”
- “It fails the build if the PDF is sloppy.”
- “Rules are paraphrased + pointer-backed; no book text shipped.”

View file

@ -0,0 +1,138 @@
# Rule Ingestion SOP (Chicago / Bringhurst pointers, no quotes)
This is the operator workflow for adding new rules to `iftypeset` without drifting into “we copied the book”.
## Nonnegotiables (repeat them until boring)
- **Never store book text** (OCR output, excerpts, paragraphs) in the repo.
- Rules are **paraphrases only** + **pointer refs**.
- Chicago OCR is allowed **ephemerally** to locate pointers; temp files must be deleted.
- If exact wording matters, the rule must say: `Exact wording required—refer to pointer`.
## What youre producing
You add a small batch file under `spec/rules/<category>/v1_<category>_<nnn>.ndjson` where each line is a JSON rule record validated by `spec/schema/rule.schema.json`.
## Step-by-step workflow
### 1) Pick the category + severity honestly
- Category: one of the `category_taxonomy` buckets in `spec/manifest.yaml`.
- Severity:
- `must`: blocks release unless profile overrides lower it
- `should`: best practice; can be warn in degraded mode
- `warn`: advisory
If it cant be automated, tag it `manual_checklist=true` and set `enforcement: manual`.
### 2) Locate the pointer (without storing text)
#### Bringhurst (text-layer usable)
- Use `tools/bringhurst_locate.py` (preferred) or `ripgrep` directly.
- Capture only:
- section identifier
- book page number (if present)
- scan page index (optional)
#### Chicago (image scan)
- Use `tools/chicago_ocr.py` **grep-only**.
- Do not copy OCR output into files.
- Use OCR to find:
- the relevant section (§)
- the printed page number
- the scan page index
Pointer format examples (note: these are *pointers*, not quotes):
- `CMOS18 §6.1 p377 (scan p10)`
- `BRING §2.3.2 p39 (scan p412)`
Rule: `(scan pN)` is a **single 1-based PDF page index**, not a range.
### 3) Write the rule record (paraphrase only)
Create a new NDJSON line. Keep `rule_text` ≤ 800 chars. Prefer short, enforceable statements.
Minimal template:
```json
{
"id": "CMOS.PUNCTUATION.DASHES.EM_DASH",
"title": "Use em dashes consistently",
"source_refs": ["CMOS18 §X.Y pNNN (scan pMMM)"],
"category": "punctuation",
"severity": "should",
"applies_to": "all",
"rule_text": "Paraphrase of the rule (no quotes). If wording matters, say: Exact wording required—refer to pointer.",
"rationale": "Why it matters (one line).",
"enforcement": "lint",
"autofix": "suggest",
"autofix_notes": "What we can safely fix (short).",
"tags": ["spacing", "manual_checklist=false"],
"keywords": ["em dash", "dash", "punctuation"],
"dependencies": [],
"exceptions": [],
"status": "draft"
}
```
Guidelines:
- **Do not** embed long examples. If you need examples, create them under `spec/examples/` and reference `examples_ref`.
- Prefer splitting cross-layer concepts into two rules:
- `lint` rule for source cleanliness
- `postrender` rule for layout outcome
- Use `dependencies` if rule ordering matters (e.g., “normalize quotes” before “ellipsis spacing”).
### 4) Tag manual rules so the checklist can be generated
If a rule requires human judgment (e.g., “choose between two valid citation styles”), set:
- `enforcement: manual`
- `tags: ["manual_checklist=true"]`
- `autofix: none`
### 5) Validate + rebuild indexes (every batch)
Run:
- `PYTHONPATH=src python3 -m iftypeset.cli validate-spec --spec spec --build-indexes`
- `PYTHONPATH=src python3 -m iftypeset.cli report --spec spec --out out --build-indexes`
Do not merge a batch if schema validation fails.
### 6) Add fixtures / examples (so rules stay enforced)
For each batch, add at least:
- 13 `spec/examples/*` entries that trigger the rule (small, targeted).
- 1 fixture doc under `fixtures/` if the rule affects real documents.
Rules without fixtures drift into “it exists but nothing enforces it.”
### 7) Promote from draft → active
Only set `status: active` when:
- the enforcement implementation exists (lint/typeset/postrender/manual)
- at least one fixture/example covers it
## Common traps (avoid)
- **Copying text into rule_text** (even “short” quotes). Dont.
- **Ranges in scan pages**: use a single `(scan pN)` hint.
- **MUST rules that are unenforceable**: tag as manual checklist or downgrade.
- **Overfitting to one document**: rules should generalize beyond a single sample.
- **“Autofix rewrite” that changes meaning**: keep fixes deterministic and reversible.
## Review checklist (before shipping a batch)
- [ ] No book text stored in repo (grep your changes).
- [ ] All rules have valid `source_refs` pointers.
- [ ] `rule_text` is paraphrase-only and short.
- [ ] Manual rules are tagged correctly.
- [ ] `validate-spec` + `report` pass.
- [ ] At least one fixture/example added for the batch.

View file

@ -0,0 +1,126 @@
# Multi-renderer Strategy (HTML→PDF adapters)
We should not bet the product on a single PDF engine. `iftypeset` should be **renderer-agnostic**: the “meaning” is in the rule registry + profiles + QA gates; the PDF renderer is an interchangeable adapter.
## Principles
- **Determinism first**: the adapter must emit `render-log.json` with engine name + version + key options.
- **No-network capable**: engines must run with `--network=none`/offline mode in CI where possible.
- **Graceful degradation**: if no PDF engine exists, HTML artifacts + HTML-based QA must still run.
- **Capability disclosure**: if a gate cant be measured with an engine, report it explicitly (dont silently pass).
## Adapter interface (contract)
All PDF engines implement the same interface:
```python
class PdfEngine(Protocol):
name: str
def is_available(self) -> bool: ...
def version(self) -> str: ...
def render(self, *, html_path: str, css_path: str, assets_dir: str | None, out_pdf: str, options: dict) -> dict:
"""Returns a structured log: timings, warnings, engine opts, feature flags."""
```
The CLI should support:
- `--engine auto|chromium|weasyprint|prince|antenna|vivliostyle|wkhtmltopdf`
- `--engine-opts <json>`
## “Majors” to target (pragmatic)
### Tier 1 (easy to run, common)
1) **Chromium / Headless browser**
- via Playwright or system Chromium (`chrome --headless --print-to-pdf`)
- Pros: ubiquitous, good HTML/CSS coverage, easy containerization.
- Cons: paged-media features vary; footnotes/running headers are limited unless carefully built.
2) **WeasyPrint**
- Pros: pure Python workflow, good paged-media support, easy CI story.
- Cons: CSS compatibility differs; some complex layouts may need workarounds.
### Tier 2 (best print fidelity; commercial)
3) **PrinceXML**
- Pros: excellent paged media, footnotes, running headers, print-quality output.
- Cons: license cost; needs binary distribution policy.
4) **Antenna House Formatter**
- Pros: top-tier print fidelity; standards publishing; robust PDF/A options.
- Cons: license + operational complexity.
### Tier 3 (useful but limited)
5) **Vivliostyle / Paged.js**
- Pros: strong paged-media model in the web ecosystem.
- Cons: heavier runtime; often “HTML+JS render” rather than simple CLI.
6) **wkhtmltopdf**
- Pros: simple deploy story in legacy environments.
- Cons: outdated rendering model; limited CSS; not ideal for “high quality”.
## Capability matrix (what we care about)
We should encode an engine capability report (per run) for:
- paged media (margins, page size, running headers)
- hyphenation support + dictionaries
- font embedding/subsetting
- link handling (wrap/break strategy)
- footnotes (if we later support them)
- PDF/A options (later)
This capability map feeds QA:
- if engine cant support a gate (e.g., true widow/orphan detection on PDF), QA should:
- run the best available approximation, and
- mark the gate as `skipped` with a reason, not `passed`.
## Determinism knobs (must record)
For every PDF render, write `out/render-log.json` including:
- engine name + version
- invocation args
- environment hints (OS, locale)
- “self-contained” mode on/off
- fonts resolved (what was available vs requested)
- any warnings from the engine
If the engine is a browser:
- fix viewport
- disable external requests
- pin print settings (margins, background graphics, scaling)
## Security model
- Assume untrusted Markdown input (CI context). Mitigations:
- never execute embedded JS during HTML render (or use a hardened renderer container)
- disable network
- restrict filesystem access (mount only `out/` and input)
- If using headless browsers, treat them as an attack surface; run in locked-down containers.
## Recommended v0.1 path (fastest)
1) Implement adapters for:
- Chromium/Playwright (auto-detect)
- WeasyPrint (if installed)
2) Keep Prince/AH as optional adapters (stub + docs) until needed.
3) Use QA gates as the real value:
- link wrap, code/table overflow, stranded headings (HTML and PDF when possible)
This keeps delivery fast while preserving “compatible with the majors”.
## Future: “Engine parity” testing
Once adapters exist, add an integration job that renders the same fixtures through 2 engines (when available) and compares:
- gate metrics (should be within thresholds)
- file size ranges
- major layout regressions (e.g., table clipping incidents)
We dont need pixel-perfect equivalence; we need “quality gates still pass”.

View file

@ -0,0 +1,236 @@
# External Evaluation Prompt — `iftypeset` (pubstyle)
**Goal:** confirm there is no fundamental flaw (technical, legal, product) and identify obvious issues early.
**Audience:** humans or LLM reviewers.
**Repo root:** `ai-workspace/iftypeset/`
## 0) Context (read this first)
`iftypeset` is a thin, deterministic publishing runtime for **Markdown → HTML → PDF** that adds:
- A **machinereadable rule registry** (rules are paraphrases only) with **pointer refs** back to primary sources (Chicago / Bringhurst) instead of reproducing book text.
- **Typeset profiles** (screen-first vs print-first vs dense tech, etc.) that map typographic intent into render tokens/CSS.
- **Postrender QA gates** that can fail builds when layout degrades (widows/orphans/keeps/overflow/link-wrap/numbering issues).
### Nonnegotiables (legal + product)
- Do **not** OCR/transcribe entire books into the repo (copyright). Rules must remain paraphrases with pointers only.
- Source pointers must be sufficient for someone who has the book to find the guidance, without quoting it.
- The runtime must be able to run in constrained environments (e.g. Forgejo PDF export workers) and produce deterministic artifacts.
## 1) What to review (map of the repo)
Start here:
- `README.md`
- `STATUS.md`
- `app/ARCHITECTURE.md`
- `app/CLI_SPEC.md`
- `docs/01-demo-acceptance.md`
- `docs/02-competitor-matrix.md`
- `docs/03-rule-ingestion-sop.md`
- `docs/04-renderer-strategy.md`
Spec + rules:
- `spec/schema/rule.schema.json`
- `spec/manifest.yaml`
- `spec/profiles/*.yaml`
- `spec/quality_gates.yaml`
- `spec/rules/**.ndjson`
- `spec/indexes/*.json` (derived; rebuildable)
Forgejo integration note:
- `forgejo/README.md`
## 2) Quick verification (local)
From `ai-workspace/iftypeset/`, run:
```bash
./scripts/ci.sh
```
Confirm it:
- validates the spec
- generates a coverage report
- runs unit tests
If it fails, include the command output in your review.
## 3) Required reviewer metadata (so we can trust the review)
### If you are a human reviewer
- `reviewer_background`: 12 lines (e.g., “publishing/typography”, “security/GRC”, “docs tooling”).
- `tools_used`: list (e.g., Prince, Antenna House, Pandoc, Quarto, LaTeX, Typst, WeasyPrint, Paged.js, DocRaptor).
- `date_utc`: ISO 8601.
### If you are an LLM reviewer
- `llm_name`: provider + model string
- `probable_model`: if ambiguous
- `cutoff_date`: YYYYMM or `unknown`
- `response_date_utc`: ISO 8601
- `web_access_used`: `yes|no`
## 4) Evaluation rubric (scorecard)
Score each category 05 and write 13 sentences of justification.
### 4.1 Product + positioning
1) **Problem clarity (05)**
Does this solve a real pain for teams shipping PDFs, beyond “another renderer”?
2) **Differentiation (05)**
Is the “rule registry + QA gates + deterministic artifacts” wedge clear and credible vs:
Pandoc/Quarto/Typst/LaTeX, Prince/AntennaHouse/WeasyPrint/Vivliostyle/Paged.js, DocRaptor, etc.?
3) **Viability (05)**
Is this buildable to a useful v0.1 in weeks (not months) with a small team?
### 4.1a Content + style (docs/readability)
11) **Docs clarity (05)**
Can a new contributor follow `README.md` and get a useful output quickly?
12) **Spec readability (05)**
Are `spec/manifest.yaml`, `spec/profiles/*.yaml`, and `spec/quality_gates.yaml` self-explanatory enough for a reviewer?
13) **Market-facing clarity (05)**
If this were shown to a buyer, does it read like a product with a clear contract, or a research project?
### 4.2 Technical architecture
4) **Spec design (05)**
Are `rule.schema.json`, `manifest.yaml`, and the profile/gate model coherent and extensible?
5) **Enforcement model (05)**
Is the split between `lint` / `typeset` / `postrender` / `manual` realistic? Are “manual checklist” rules handled honestly?
6) **Determinism strategy (05)**
Does the repo clearly define what “deterministic” means (inputs, renderer versions, fonts, outputs)?
### 4.3 Rules + content quality
7) **Rule record quality (05)**
Do rule records look like paraphrases with pointers (not copied text)? Are IDs/tags/keywords useful?
8) **Coverage strategy (05)**
Are we prioritizing the right categories first (numbers/punctuation/citations/layout), and is coverage reporting useful?
### 4.4 UX / operational usability
9) **CLI ergonomics (05)**
Is the CLI spec clear for CI usage (exit codes, JSON artifacts, strictness flags)?
10) **Integration story (05)**
Is Forgejo integration plausible and incremental (CSS first, then QA gates)?
### 4.5 Market viability (compare to existing options)
Rate each 05 based on *your experience* (no need to be exhaustive; avoid vendor hype).
14) **Replace vs complement (05)**
Is `iftypeset` best positioned as a replacement for existing toolchains, or as a QA layer you plug into them?
15) **Who pays first (05)**
Does the repo make it clear who would adopt/pay first (docs teams, GRC, legal, research, vendors)?
16) **Defensible wedge (05)**
Is “publishing CI with hard QA gates + auditable rule registry” a defensible wedge, or easy for existing tools to add?
## 5) “Fundamental flaw” checklist (answer explicitly)
Mark each: `PASS` / `RISK` / `FAIL`, with a oneline explanation.
1) **Copyright / licensing risk**
Any sign the repo is storing book text rather than paraphrases + pointers?
2) **Determinism risk**
Are we likely to produce different PDFs across machines/runs due to fonts/renderer drift?
3) **QA gate feasibility**
Are the proposed post-render QA gates realistically implementable, or is this a research project?
4) **Scope creep risk**
Does the plan keep a narrow v0.1 “definition of done”, or is it trying to boil the ocean?
5) **Market reality**
Is there a clear “why buy/use this” vs adopting an existing doc toolchain and living with some ugliness?
## 5a) Section-by-section ratings (required)
Rate each **05** and include 12 lines of justification. The goal is to catch “obvious issues” early.
- `README.md`: clarity + truthfulness (does it match current behavior?)
- `STATUS.md`: accuracy + usefulness (is it a reliable snapshot?)
- `app/ARCHITECTURE.md`: coherence + feasibility
- `app/CLI_SPEC.md`: completeness + CI friendliness
- `docs/01-demo-acceptance.md`: crisp v0.1 target or scope creep?
- `docs/02-competitor-matrix.md`: honest + actionable (no wishful marketing)
- `docs/03-rule-ingestion-sop.md`: safe + repeatable (avoids copyright drift)
- `docs/04-renderer-strategy.md`: realistic adapter plan
- `spec/manifest.yaml`: enforceable contracts + degraded mode clarity
- `spec/schema/rule.schema.json`: schema quality (strict enough, not brittle)
- `spec/profiles/*.yaml`: profiles feel sane, not arbitrary
- `spec/quality_gates.yaml`: gates are measurable + meaningful
- `spec/rules/**.ndjson`: rule quality (paraphrase + pointer discipline)
## 6) Deliverables quality (what “good” looks like)
Assess whether the repo is on track to produce, for a single Markdown input:
- `render.html` + `render.css` (deterministic)
- `render.pdf` (deterministic *given pinned engine/fonts*)
- `lint-report.json`
- `layout-report.json`
- `qa-report.json` (pass/fail thresholds)
- `coverage-report.json` (rule implementation progress)
- `manual-checklist.md` (for rules that cannot be automated)
If you think any of these deliverables are unnecessary or missing, say so.
## 7) Patch suggestions (actionable)
Provide 515 suggestions in this format:
- `target`: file path(s)
- `problem`: 1 sentence
- `change`: concrete text/code change (copy/pasteable)
- `why`: 1 sentence
- `priority`: P0 / P1 / P2
- `confidence`: high / medium / low
### Preferred patch format
If possible, include unified diffs:
```diff
--- a/path/file.md
+++ b/path/file.md
@@
...
```
## 8) Output template (copy/paste)
Use this structure in your response:
1) **Summary (510 bullets)**
2) **Scorecard (05 each)**
3) **Fundamental flaw checklist (PASS/RISK/FAIL)**
4) **Top risks (P0/P1)**
5) **Patch suggestions (with diffs if possible)**
6) **Go / NoGo recommendation for v0.1**
## 9) Important constraint for reviewers
Do not paste verbatim passages from Chicago/Bringhurst into your review output. Use pointers only (e.g., `BRING §2.1.8 p32`) and describe the issue in your own words.
## 10) Quick market question (optional, but useful)
If you had to ship “good-looking PDFs with hard QA gates” tomorrow, what would you use today, and why would you still choose `iftypeset` (or not)?

View file

@ -0,0 +1,73 @@
# iftypeset (pubstyle) — project overview
This document is a narrative snapshot meant for handoffs and external reviewers.
## Where we come from
Most Markdown→PDF pipelines optimize for “it renders” and stop there. In practice, teams who ship PDFs for real audiences (customers, regulators, courts, boards) care about the *failure modes*:
- links that wrap into unreadable fragments
- tables that overflow or clip
- headings stranded at the bottom of a page
- inconsistent numbering/citations
- “looks fine on my machine” drift when renderers/fonts change
`iftypeset` starts from a simple premise: **quality must be measurable and enforceable**.
## Where we are (today)
We have a working foundation that is intentionally boring:
- A **machine-readable rule registry** (`spec/rules/**.ndjson`) that stores *paraphrased* rules and **pointer refs** back to primary sources (Chicago / Bringhurst), without reproducing book text.
- A **profile system** (`spec/profiles/*.yaml`) that maps typographic intent into deterministic render tokens (page size, margins, font stacks, measure targets, hyphenation policy).
- **Post-render QA gates** (`spec/quality_gates.yaml`) that define hard numeric thresholds for layout failures.
- A working CLI surface (`iftypeset.cli`) that can validate the spec, emit coverage reports, lint Markdown, render HTML/CSS, render PDF (via available engines), and run QA.
Current progress is tracked in `STATUS.md` and `out/coverage-summary.md`.
## Where we are going (v0.1 → v1)
### v0.1: “Publishing CI” for a single Markdown input
The v0.1 goal is *not* to be the best renderer. Its to be the most reliable pipeline:
- deterministic HTML/CSS output for a chosen profile
- PDF generation via adapters (Chromium first, others later)
- QA reports that catch common layout failures and fail the build when thresholds are exceeded
- an honest manual checklist for rules that cannot be automated
Definition of done lives in `docs/01-demo-acceptance.md`.
### v0.2+: broaden rule coverage + deepen QA gates
Once the pipeline is stable, we expand breadth and depth:
- add more rule categories (figures, frontmatter/backmatter, abbreviations, i18n, accessibility)
- increase post-render QA coverage (widows/orphans, keep constraints, overfull lines)
- add more fixtures to harden degraded-mode handling
### v1: “adapter-compatible” quality gates
Longer-term, `iftypeset` should work with the majors:
- keep the “meaning” in profiles + QA, not in a single renderer
- support swapping PDF engines without losing the ability to measure quality consistently
Renderer strategy is documented in `docs/04-renderer-strategy.md`.
## Traps to avoid (so we dont drift)
- **Copying book text into the repo:** we can use OCR to locate pointers, but we must not persist verbatim passages.
- **Pretending manual rules dont exist:** if it cant be enforced, it must land in `manual-checklist.md` with a pointer.
- **Overfitting to one renderer:** adapters are the point; pinning is allowed, lock-in is not.
- **Unmeasurable QA gates:** if we cant measure it reliably, its a “should” or “manual”, not a “must”.
## Why this is valuable
The differentiator is not “Markdown to PDF”. Its:
**A) auditable rules** (paraphrase + pointer discipline) and
**B) enforceable layout QA** (fail the build when its sloppy).
Thats what makes it compatible with governance workflows (hash/sign artifacts, attach QA reports, reproduce later) and usable in constrained CI environments (like Forgejo PDF export workers).

View file

@ -0,0 +1,40 @@
# Session Resilience (avoid “lost work” when chats reset)
Codex/chat sessions can lose **conversation context** when a connection drops. The filesystem does not: the durable source of truth is the repo.
This project adds a few boring mechanisms to make resuming work deterministic.
## What to trust
- **Repo state**: `README.md`, `STATUS.md`, `docs/` are canonical.
- **CI**: `./scripts/ci.sh` is the fastest sanity check.
- **Artifacts**: `out/` contains the latest reports from CI runs.
## Quick resume checklist (30 seconds)
From the repo root:
- `./scripts/audit.sh`
- `./scripts/ci.sh`
If both look sane, youre back.
## Create a checkpoint (2 minutes)
When you finish a meaningful chunk of work (new rule batches, QA changes, renderer changes), run:
- `./scripts/checkpoint.sh "what changed"`
This:
- runs CI and stores the CI JSON in `out/checkpoints/`
- creates a compressed snapshot tarball in `out/checkpoints/`
- appends a new entry to `docs/CHECKPOINTS.md` with the snapshot hash
This gives you a **portable restore point** even if the chat transcript is gone.
## Best practice (recommended)
- Push to a remote early (Forgejo/GitHub). A remote is the best anti-loss mechanism.
- Treat `STATUS.md` as the “1-page truth” for what exists and whats next.
- Dont rely on chat logs for state; copy any critical decisions into `docs/`.

17
docs/CHECKPOINTS.md Normal file
View file

@ -0,0 +1,17 @@
# Checkpoints
Durable restore points for this repo (useful when chat context resets).
How to create a checkpoint:
- `./scripts/checkpoint.sh "short note about what changed"`
Each entry records the snapshot tarball path + sha256 and the CI JSON for that moment.
## 2026-01-03T20:09:44Z
- snapshot: `out/checkpoints/iftypeset_checkpoint_2026-01-03T20-09-44Z.tar.gz`
- snapshot_sha256: `95583b843415cbb39de3199b1a73453d163f858b869b1852799360b6ca35388a`
- ci_json: `out/checkpoints/ci_2026-01-03T20-09-44Z.json`
- note: first checkpoint test

View file

@ -0,0 +1,3 @@
# Abbreviations
Define abbreviations on first use, for example "Minimal Viable Product (MVP)".

View file

@ -0,0 +1,3 @@
# Accessibility
![Alt text](images/example.png)

View file

@ -0,0 +1,5 @@
# Back Matter
## Appendix A
Supplemental material goes in appendices.

View file

@ -0,0 +1,4 @@
# Author-Date
Use author-date citations in contexts where quick lookup is needed, such as
Smith 2024, 15-18.

View file

@ -0,0 +1,4 @@
# Bibliography
List sources in a consistent order, and keep formatting consistent across
entries.

View file

@ -0,0 +1,4 @@
# Notes
Use notes to provide sources. Keep notes concise, and ensure they include
locators when referencing specific passages.

3
fixtures/code_inline.md Normal file
View file

@ -0,0 +1,3 @@
# Inline Code
Use `inline_code()` for identifiers and small code tokens.

View file

@ -0,0 +1,5 @@
# Long Code Lines
```text
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
```

View file

@ -0,0 +1,3 @@
# Figures
Figure 1 shows a placeholder description for figure handling.

View file

@ -0,0 +1,5 @@
# Front Matter
## Preface
Introductory material lives here.

View file

@ -0,0 +1,9 @@
# Headings Overview
## Subsection One
Content under the first subsection.
## Subsection Two
Content under the second subsection.

View file

@ -0,0 +1,5 @@
# Main Title
### Skipped Heading Level
This heading skips a level to test lint.

View file

@ -0,0 +1,7 @@
# 1 Introduction
## 1.1 Background
### 1.1.1 Detail
Text for a numbered heading sequence.

3
fixtures/hyphenation.md Normal file
View file

@ -0,0 +1,3 @@
# Hyphenation
Use hyphenation only when it improves clarity in compound modifiers.

3
fixtures/i18n_quotes.md Normal file
View file

@ -0,0 +1,3 @@
# International Quotes
Use locale-appropriate quotation styles when writing in non-English contexts.

View file

@ -0,0 +1,5 @@
# Page Breaks
## Section One
Content that should stay with the heading.

View file

@ -0,0 +1,6 @@
# Layout Spacing
This paragraph is followed by another paragraph that should preserve
consistent spacing in the rendered output.
This is the follow-up paragraph.

5
fixtures/layout_widow.md Normal file
View file

@ -0,0 +1,5 @@
# Widow Risk
Short line.
Another short line.

View file

@ -0,0 +1,3 @@
# Bare URL
Use a bare URL like http://example.com when you need the literal address.

View file

@ -0,0 +1,4 @@
# Long URL
A very long URL may wrap poorly in narrow measures:
https://example.com/this/is/an/extraordinarily/long/url/with/no/breaks/that/should/trigger/qa

View file

@ -0,0 +1,5 @@
# Ordered List
1. First step
2. Second step
3. Third step

View file

@ -0,0 +1,5 @@
# Unordered List
- Alpha
- Beta
- Gamma

View file

@ -0,0 +1,3 @@
# Currency
Use consistent currency formatting, such as USD 1,200.00 or $1,200.00.

View file

@ -0,0 +1,4 @@
# Dates
Use a consistent date format. For example, 2025-01-03 in ISO form or
January 3, 2025 in US prose.

View file

@ -0,0 +1,3 @@
# Inclusive Ranges
Use en dashes for ranges like 12-24 and avoid ambiguous shortening.

View file

@ -0,0 +1,4 @@
# Numbers in Prose
Spell out numbers in running text when they are short and not attached to
units. Use numerals in technical contexts.

View file

@ -0,0 +1,4 @@
# Colons
Use a colon after a complete clause when introducing a list: for example,
items, examples, and exceptions.

View file

@ -0,0 +1,4 @@
# Commas in Lists
Use commas to separate items in a series, and use a serial comma where
clarity matters. Avoid comma splices in formal prose.

View file

@ -0,0 +1,4 @@
# Dashes
Use em dashes for breaks in thought and en dashes for numeric ranges such
as 2019-2024. Avoid mixing dash types within a single paragraph.

View file

@ -0,0 +1,4 @@
# Quotation Marks
Use double quotation marks for direct quotations in US English and single
quotation marks for quotes within quotes.

View file

@ -0,0 +1,4 @@
# Semicolons
Use semicolons between related independent clauses; avoid overusing them
when a period is clearer.

29
fixtures/sample.md Normal file
View file

@ -0,0 +1,29 @@
# Sample Document
This sample exists to exercise lint, render, and QA. It includes a long URL
and a code block that may overflow on narrow measures.
## Links
Visit [Example](https://example.com/docs) for context.
Here is a long URL that might wrap: https://example.com/this/is/a/very/long/path/with/no/obvious/breaks/and/it/keeps/going
## Lists
- First item
- Second item with two spaces to trigger lint
1. First ordered item
2. Second ordered item
## Table
| Column A | Column B | Column C | Column D | Column E | Column F |
| --- | --- | --- | --- | --- | --- |
| Short | Short | Short | Short | Short | Short |
## Code
```python
print("This is a very long line that should trigger overflow checks in QA because it exceeds typical measures")
```

View file

@ -0,0 +1,5 @@
# Table Alignment
| Left | Center | Right |
| :--- | :---: | ---: |
| L | C | R |

6
fixtures/tables_basic.md Normal file
View file

@ -0,0 +1,6 @@
# Basic Table
| Name | Value |
| --- | --- |
| Alpha | 10 |
| Beta | 20 |

5
fixtures/tables_wide.md Normal file
View file

@ -0,0 +1,5 @@
# Wide Table
| C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| A | B | C | D | E | F | G | H |

View file

@ -0,0 +1,3 @@
# Font Stacks
Choose serif and sans-serif stacks that match the target medium.

117
forgejo/README.md Normal file
View file

@ -0,0 +1,117 @@
# Forgejo PDF integration (iftypeset → forgejo-pdf worker)
This note documents how to wire `iftypeset` into the existing Forgejo PDF worker so exported PDFs stop feeling “flat” and start behaving like a real typesetting pipeline.
## Current state (Forgejo worker)
The current renderer lives at:
- `/root/ai-workspace/forgejo-pdf/worker/pdf/src/render_pdf.js`
It currently:
- Converts Markdown → HTML (MarkdownIt + sanitize-html).
- Renders Mermaid diagrams in-page.
- Uses Paged.js for pagination.
- Emits a PDF via Puppeteer/Chromium.
- Applies one of two static stylesheets:
- `basic.css`
- `professional.css`
## What iftypeset adds
`iftypeset` is a deterministic “rules + profiles + QA gates” layer.
In Forgejo terms:
- **Profiles** (`spec/profiles/*.yaml`) → deterministic CSS tokens (`iftypeset emit-css`).
- **Quality gates** (`spec/quality_gates.yaml`) → post-render checks (widows/orphans, overflow, stranded headings, etc.) with hard numeric thresholds.
- **Rule registry (Phase 2)** → lint + manual checklists (Chicago/Bringhurst pointers, paraphrased).
## Minimal integration (CSS only, low risk)
1. Generate CSS from a profile:
```bash
cd /root/ai-workspace/iftypeset
PYTHONPATH=src python3 -m iftypeset.cli emit-css --spec spec --profile web_pdf --out /tmp/iftypeset-css
```
2. Copy CSS into the worker assets directory:
```bash
cp /tmp/iftypeset-css/render.css /root/ai-workspace/forgejo-pdf/worker/pdf/assets/css/iftypeset-web_pdf.css
```
3. Add a new `pdf.typography` option in the worker config contract (example):
- `basic`
- `professional`
- `iftypeset-web_pdf` (new)
4. Update `render_pdf.js` selection logic to map that value to a CSS filename in `assets/css/`.
This is the safest first step: no new dependencies in the worker container, no new runtime calls, just a different stylesheet.
## Next integration (QA gates, medium risk)
The goal is to produce:
- `layout-report.json` (measured layout incidents)
- `qa-report.json` (gate pass/fail summary)
at export time.
Recommended approach:
1. **Pre-PDF** (in-page, after Paged.js preview):
- collect page count
- collect per-page heading positions (to detect “stranded headings”)
- record overflow signals (code blocks / tables that exceed page content boxes)
2. **Post-PDF** (optional, later):
- parse the PDF with a dedicated analyzer to detect widows/orphans more accurately
Start with the in-page signals first because the Forgejo worker already owns the DOM and pagination lifecycle.
## CI wiring (recommended)
In a Forgejo job, run the pipeline after Markdown is available:
```bash
PYTHONPATH=src python3 -m iftypeset.cli lint --input <doc.md> --out out --profile web_pdf
PYTHONPATH=src python3 -m iftypeset.cli render-html --input <doc.md> --out out --profile web_pdf
PYTHONPATH=src python3 -m iftypeset.cli render-pdf --input <doc.md> --out out --profile web_pdf || true
PYTHONPATH=src python3 -m iftypeset.cli qa --out out --profile web_pdf
```
Artifacts to publish (static hosting):
- `out/render.html`
- `out/render.css`
- `out/render.pdf` (if available)
- `out/layout-report.json`
- `out/qa-report.json`
- `out/lint-report.json`
Failures should be surfaced via exit codes and `qa-report.json` (gate failures list).
## Fonts (important)
Forgejos `professional.css` embeds IBM Plex via `@font-face`.
If you switch to `iftypeset` CSS profiles as-is, you should either:
- add the fonts used by the profile to the worker assets (preferred for consistency), or
- update the profile `fonts.*.family` stacks to prefer the fonts already bundled in the worker (`IBM Plex Sans WOFF2`, `IBM Plex Mono WOFF2`).
## Long-term direction
Once Phase 2 rule batches exist (`spec/rules/**.ndjson`), Forgejo can become a full “publication pipeline”:
- `iftypeset lint` → deterministic lint report + optional autofix (no quotes from books, pointers only)
- `iftypeset emit-css` → render tokens
- Forgejo render → HTML/PDF
- `iftypeset qa` → gate failures block the PDF build in CI
This keeps the worker simple and lets the strictness live in the spec, not ad-hoc code.

16
pyproject.toml Normal file
View file

@ -0,0 +1,16 @@
[project]
name = "iftypeset"
version = "0.1.0"
description = "Publication-quality typesetting runtime (Markdown→HTML→PDF) with Chicago/Bringhurst-backed rule registry pointers."
requires-python = ">=3.11"
dependencies = [
"pyyaml>=6.0",
"jsonschema>=4.19",
]
[project.scripts]
iftypeset = "iftypeset.cli:main"
[tool.pytest.ini_options]
testpaths = ["tests"]

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
PyYAML==6.0.2
jsonschema==4.19.2

56
scripts/audit.sh Executable file
View file

@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo "iftypeset audit @ ${ts}"
echo "root: ${ROOT}"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "git: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?') $(git rev-parse --short HEAD 2>/dev/null || echo 'no-commit')"
git status --porcelain=v1 -b || true
else
echo "git: (not a git repo)"
fi
if [ -f out/coverage-report.json ]; then
python3 - <<'PY'
import json
from pathlib import Path
d = json.loads(Path("out/coverage-report.json").read_text())
counts = d.get("counts", {})
print("coverage:")
print(f" total_rules: {counts.get('total_rules')}")
print(f" by_category: {counts.get('by_category')}")
print(f" by_enforcement: {counts.get('by_enforcement')}")
print(f" by_severity: {counts.get('by_severity')}")
print(f" generated_at_utc: {d.get('generated_at_utc')}")
PY
else
echo "coverage: out/coverage-report.json missing (run ./scripts/ci.sh)"
fi
echo "recent files:"
ls -lt README.md STATUS.md spec/manifest.yaml 2>/dev/null | sed -n '1,5p' || true
ls -lt src/iftypeset 2>/dev/null | sed -n '1,12p' || true
echo "checkpoints:"
if ls out/checkpoints/iftypeset_checkpoint_*.tar.gz >/dev/null 2>&1; then
latest="$(ls -1t out/checkpoints/iftypeset_checkpoint_*.tar.gz | head -n 1)"
latest_sha="$(sha256sum "$latest" | awk '{print $1}')"
echo " latest_snapshot: ${latest}"
echo " latest_snapshot_sha256: ${latest_sha}"
else
echo " latest_snapshot: (none) # run ./scripts/checkpoint.sh"
fi
if [ -f docs/CHECKPOINTS.md ]; then
echo " docs/CHECKPOINTS.md: present"
tail -n 14 docs/CHECKPOINTS.md | sed 's/^/ | /'
else
echo " docs/CHECKPOINTS.md: missing"
fi

65
scripts/checkpoint.sh Executable file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
safe_ts="$(date -u +"%Y-%m-%dT%H-%M-%SZ")"
note="${1:-}"
mkdir -p out/checkpoints
echo "== running ci ==" >&2
ci_json="$(./scripts/ci.sh)"
echo "$ci_json" > "out/checkpoints/ci_${safe_ts}.json"
tar_path="out/checkpoints/iftypeset_checkpoint_${safe_ts}.tar.gz"
echo "== creating snapshot ${tar_path} ==" >&2
tar \
--exclude=".git" \
--exclude=".venv" \
--exclude="out" \
-czf "$tar_path" \
.forgejo .gitignore README.md STATUS.md app docs fixtures forgejo pyproject.toml requirements.txt scripts spec src tests tools
sha="$(sha256sum "$tar_path" | awk '{print $1}')"
cat > "out/checkpoints/checkpoint_${safe_ts}.md" <<EOF
# iftypeset checkpoint
- timestamp_utc: ${ts}
- snapshot: ${tar_path}
- snapshot_sha256: ${sha}
- ci: \`out/checkpoints/ci_${safe_ts}.json\`
$( [ -n "${note}" ] && printf -- "- note: %s\n" "${note}" )
EOF
if [ ! -s docs/CHECKPOINTS.md ]; then
cat > docs/CHECKPOINTS.md <<'EOF'
# Checkpoints
Durable restore points for this repo (useful when chat context resets).
How to create a checkpoint:
- `./scripts/checkpoint.sh "short note about what changed"`
Each entry records the snapshot tarball path + sha256 and the CI JSON for that moment.
EOF
fi
cat >> docs/CHECKPOINTS.md <<EOF
## ${ts}
- snapshot: \`${tar_path}\`
- snapshot_sha256: \`${sha}\`
- ci_json: \`out/checkpoints/ci_${safe_ts}.json\`
$( [ -n "${note}" ] && printf -- "- note: %s\n" "${note}" )
EOF
echo "checkpoint complete: ${tar_path} (sha256=${sha})" >&2

26
scripts/ci.sh Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
status=0
json_entries=()
run_step() {
local name="$1"
shift
"$@"
local rc=$?
if [ $rc -ne 0 ]; then
status=1
fi
json_entries+=("{\"step\":\"${name}\",\"rc\":${rc}}")
}
run_step "validate-spec" env PYTHONPATH=src python3 -m iftypeset.cli validate-spec --spec spec --build-indexes
run_step "report" env PYTHONPATH=src python3 -m iftypeset.cli report --spec spec --out out --build-indexes
run_step "tests" python3 -m unittest discover -s tests -p 'test_*.py'
printf '{"ok":%s,"steps":[%s]}\n' "$([ $status -eq 0 ] && echo true || echo false)" "$(IFS=,; echo "${json_entries[*]}")"
exit $status

88
spec/examples/README.md Normal file
View file

@ -0,0 +1,88 @@
# Examples
Rules stay compact and machine-enforceable; examples live separately to avoid bloating the rule registry.
## Goals
* Provide **concrete fixtures** for:
* unit tests (lint, autofix, typeset transforms)
* integration tests (render + QA gates)
* documentation (human-readable “why this matters”)
* Keep examples **small** (a few lines) and **targeted** (each example triggers a known set of rules).
## Example ID format
`EX.<CATEGORY>.<TOPIC>.<NNN>`
* `CATEGORY` must match the category taxonomy (e.g., `PUNCTUATION`, `NUMBERS`, `CITATIONS`)
* `TOPIC` is an uppercase short slug
* `NNN` is a zero-padded integer (000999+)
Example:
* `EX.PUNCTUATION.DASHES.001`
## Suggested on-disk layout
* `spec/examples/<category>/EX.<CATEGORY>.<TOPIC>.<NNN>.yaml`
* `spec/examples/<category>/fixtures/<name>.md` (optional)
## Example YAML format (recommended)
Fields:
* `id` (required): example ID
* `rules` (required): list of rule IDs the example is meant to exercise
* `before` (required): inline Markdown or a reference to a fixture file
* `after` (optional): expected Markdown after autofix (if autofix exists)
* `expected` (optional): expected diagnostics/gates
* `lint_errors`: array of rule IDs expected as errors
* `lint_warnings`: array of rule IDs expected as warnings
* `qa_failures`: array of gate keys expected to fail
* `notes` (optional): short human explanation (no book quotes)
Minimal example skeleton:
```yaml
id: EX.PUNCTUATION.DASHES.001
rules:
- CMOS.PUNCTUATION.DASHES.EM_DASH
before: |
...
after: |
...
expected:
lint_errors:
- CMOS.PUNCTUATION.DASHES.EM_DASH
```
## Test corpus strategy
Maintain a small, curated corpus that triggers:
1. Lint-only issues (AST-level)
* punctuation spacing
* numeral formatting
* heading numbering patterns
* link normalization / unsafe URLs
* citation field completeness
2. Typeset-only issues (token/CSS decisions)
* paragraph indentation patterns
* code block wrapping rules
* table overflow strategies
3. Post-render QA issues (PDF/HTML layout)
* widows/orphans
* stranded headings (keep-with-next)
* overfull lines (especially monospace/code)
* table/caption overflow and clipping
* link wrap incidents (URLs/DOIs split against policy)
Recommended corpus sizing:
* 3080 fixtures total
* each fixture should target 310 rules max
* include “degraded mode” fixtures (intentionally malformed Markdown)

112
spec/extraction_plan.md Normal file
View file

@ -0,0 +1,112 @@
# Phase 2 Extraction Plan
This plan defines how rules will be produced in controlled batches without reproducing the books.
## Non-negotiables (carried into Phase 2)
* No full-book OCR/transcription.
* No long verbatim passages.
* Rules are paraphrased and capped (`rule_text` ≤ 800 chars).
* Every rule includes at least one source pointer in `source_refs`.
* If a rule depends on exact wording, the rule still paraphrases but must include:
* `rule_text`: “Exact wording required—refer to pointer”
* plus a usable pointer.
Primary reference PDFs for pointer extraction:
* The Chicago Manual of Style (18th ed).pdf
* Robert Bringhurst The Elements of Typographic Style.pdf
## Output batching format
When you say: `EXTRACT <CATEGORY> [<SCOPE>]`
I will output a bundle that includes:
1. **Rules NDJSON** (150250 rule records)
* Path: `spec/rules/<category>/<batch_id>.ndjson`
* One JSON object per line, validated against `spec/schema/rule.schema.json`.
2. **Index deltas** for that category
* `spec/indexes/keywords_<category>.json`
* `spec/indexes/source_refs_<category>.json`
* `spec/indexes/coverage_delta_<category>.json`
3. **Coverage notes** report
* Short Markdown report describing enforcement split (lint/typeset/postrender/manual)
* plus any known gaps or manual-only areas
## Batch naming
`<batch_id>` format:
* `v1_<category>_<nnn>` (e.g. `v1_punctuation_001`)
Batches are append-only:
* If rules need revision, mark old rule `deprecated`, add a new rule ID (or new version segment) and keep both records.
## Pointer scheme details
Pointer strings live in `source_refs[]` and are **not** quotes.
Preferred pointer format:
* `CMOS18 §<section> p<book_page>`
* `BRING §<section> p<book_page>`
* Optional disambiguation: `(scan p<pdf_page_index>)`
Example pattern (not a quote):
* `CMOS18 §6.1 p377 (scan p10)`
Notes:
* “book_page” uses the printed page number in the book when present (arabic or roman).
* “scan p” uses the PDF page index when printed page numbers are ambiguous.
## Recommended extraction order (high-impact first)
1. numbers
2. punctuation
3. citations
4. headings
5. tables
6. figures
7. links
8. code
9. layout (widows/orphans, keeps, overflow)
10. front/back matter
11. accessibility
12. i18n
Rationale:
* Numbers/punctuation/citations most directly affect correctness and consistency.
* Layout rules benefit from having structure and tokens in place.
## Enforcement mapping guidelines (honest labeling)
* `lint`: detectable from AST or text normalization (spacing, punctuation patterns, citation fields).
* `typeset`: enforced via CSS/tokens/paged-media decisions.
* `postrender`: requires layout inspection after rendering.
* `manual`: cannot be reliably automated; must include `tags: ["manual_checklist=true"]` and be emitted into checklist outputs.
If a concept spans multiple enforcement layers:
* Prefer splitting into two rules:
* one lint rule (source cleanliness)
* one postrender rule (layout outcome)
* Use `dependencies` to link them.
## “Degraded mode” considerations during extraction
For each category batch, include some rules that specifically target degraded inputs:
* hard-wrap repair suggestions
* heading inference warnings
* link sanitation and encoding fixes
* Unicode normalization notes
These rules should generally be `warn` or `should`, unless they prevent corruption (then `must`).

59
spec/house/HOUSE_RULES.md Normal file
View file

@ -0,0 +1,59 @@
# HOUSE rule pointers (iftypeset)
This document provides pointer anchors for HOUSE rules in the `spec/rules/**.ndjson` registry.
It intentionally contains **no** excerpts from third-party books. HOUSE rules document:
- runtime/QA constraints we enforce in CI (overflow, keeps, deterministic output),
- integration expectations for upstream renderers (e.g., Forgejo PDF worker),
- how we map rule enforcement to lint/typeset/postrender/manual.
Pointers use the pattern: `HOUSE §<section> p<page>`.
## §QA.OVERFLOW (p1)
House definition of overflow incidents (overfull lines, clipped tables/code) and required handling:
- prefer wrapping and reflow,
- allow bounded font scaling only within profile limits,
- never clip silently.
## §QA.KEEPS (p1)
House definition of keep constraints:
- headings must not strand at the bottom of a page,
- keep-with-next thresholds are configured per profile.
## §QA.LINK_WRAP (p2)
House definition of link wrapping / line-breaking policy:
- prefer readable labels over raw URLs in running text,
- avoid breaking URLs in ways that change meaning (scheme/host),
- if a URL must wrap, prefer breaking at safe separators and never clip silently.
## §QA.TABLE_OVERFLOW (p2)
House definition of table overflow handling:
- tables must not clip off-page,
- prefer wrap → shrink (bounded) → rotate only when explicitly enabled by profile,
- repeat table headers on subsequent pages when supported.
## §QA.CODE_OVERFLOW (p2)
House definition of code block overflow handling:
- code must be copy/pasteable and readable without horizontal scrolling where possible,
- prefer wrap → shrink (bounded) → optional “scroll indicator” affordance for print targets,
- never clip code.
## §A11Y.BASICS (p2)
House baseline accessibility rules for exported HTML/PDF:
- heading hierarchy must be well-formed,
- images must have alt text (or an explicit empty alt when decorative),
- link text must be descriptive (avoid “click here”),
- document language must be declared where supported.

74
spec/indexes/README.md Normal file
View file

@ -0,0 +1,74 @@
# Indexes
This project builds small, fast indexes so the runtime can answer questions like:
* “Which rules mention *en dash*?”
* “Which rules cite *CMOS18 §6.88 p412*?”
* “Which rules apply to `postrender` QA?”
* “What rules are overridden by the `print_pdf` profile?”
Indexes are derived artifacts (rebuildable) and should not be hand-edited.
## Indexes the app will build
### 1) keyword → rule IDs
**Purpose:** fast search/autocomplete and lint explanations.
* **Path:** `spec/indexes/keywords_all.json` and per-category deltas:
* `spec/indexes/keywords_<category>.json`
* **Format (JSON):**
* keys: normalized keyword (lowercased)
* values: array of rule IDs sorted stable (lexicographic)
Normalization (default):
* Unicode NFKC
* lowercase
* collapse whitespace
* strip surrounding punctuation
### 2) source_ref → rule IDs
**Purpose:** audit trail back to references without embedding book text.
* **Path:** `spec/indexes/source_refs_all.json` and per-category deltas:
* `spec/indexes/source_refs_<category>.json`
* **Format (JSON):**
* keys: exact `source_ref` pointer strings
* values: array of rule IDs
### 3) category → rule IDs
**Purpose:** batch reporting, extraction coverage, profile scoping.
* **Path:** `spec/indexes/category.json`
* **Format (JSON):**
* keys: category name
* values: array of rule IDs
### 4) enforcement → rule IDs
**Purpose:** quickly decide which engine (lint/typeset/postrender/manual) handles which rules.
* **Path:** `spec/indexes/enforcement.json`
### 5) profile overrides
**Purpose:** allow profiles to override severity or token parameters without editing rules.
* **Path:** `spec/indexes/profile_overrides.json`
* **Format (JSON):**
* per profile: list of override objects (selector + action)
* selectors may match category, tags, applies_to, or explicit rule IDs
## Build guarantees
* Index builds are deterministic from:
* `spec/rules/**.ndjson`
* `spec/profiles/*.yaml`
* `spec/manifest.yaml`
* The runtime must treat indexes as **cacheable**:
* if index missing/outdated → rebuild or fallback to scanning rule files.

329
spec/indexes/category.json Normal file
View file

@ -0,0 +1,329 @@
{
"accessibility": [
"HOUSE.A11Y.DOCUMENT_LANGUAGE.DECLARE",
"HOUSE.A11Y.HEADINGS.NO_SKIPS",
"HOUSE.A11Y.IMAGES.ALT_REQUIRED",
"HOUSE.A11Y.LINK_TEXT.DESCRIPTIVE"
],
"citations": [
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.DIRECT_QUOTES",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.EXTRA_INFO",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.LOCATORS",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.MULTI_SOURCES",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.PARENTHETICAL",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.PLACEMENT",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.ALPHABETICAL",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.ORDER_AND_YEAR",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.REPEATED_NAMES",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.REQUIRED",
"CMOS.CITATIONS.BIBLIOGRAPHY.ALT_NAMES.CROSSREF",
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.CONSISTENT_FORM",
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.INITIALS_PREFERRED",
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.PUBLISHED_FORM",
"CMOS.CITATIONS.BIBLIOGRAPHY.MULTI_AUTHORS.ORDER",
"CMOS.CITATIONS.BIBLIOGRAPHY.NO_AUTHOR.TITLE_LEAD",
"CMOS.CITATIONS.BIBLIOGRAPHY.PSEUDONYMS.CONSISTENT",
"CMOS.CITATIONS.BIBLIOGRAPHY.REPEATED_NAMES.THREE_EM_DASH",
"CMOS.CITATIONS.BIBLIOGRAPHY.SAME_AUTHOR.ORDER",
"CMOS.CITATIONS.BIBLIOGRAPHY.SAME_SURNAME.DISAMBIGUATE",
"CMOS.CITATIONS.DEGRADED.NOTE_MARKERS.REPAIR",
"CMOS.CITATIONS.DEGRADED.URL_LINEBREAKS.NORMALIZE",
"CMOS.CITATIONS.DOI.PREFERRED_OVER_URL",
"CMOS.CITATIONS.IBID.MINIMIZE_OR_AVOID",
"CMOS.CITATIONS.LEGAL_PUBLIC_DOCS.USE_JURISDICTIONAL_FORMAT",
"CMOS.CITATIONS.MATCHING.BIBLIO_ENTRY_REQUIRED",
"CMOS.CITATIONS.NOTES.AUTHOR_DATE_PLUS_NOTES",
"CMOS.CITATIONS.NOTES.AVOID_OVERLONG",
"CMOS.CITATIONS.NOTES.CHAPTER_IN_EDITED_BOOK",
"CMOS.CITATIONS.NOTES.ENDNOTES.AVOID_IBID",
"CMOS.CITATIONS.NOTES.ENDNOTES.PLACEMENT",
"CMOS.CITATIONS.NOTES.ENDNOTES.RUNNING_HEADS",
"CMOS.CITATIONS.NOTES.FOOTNOTES.PAGE_BREAKS",
"CMOS.CITATIONS.NOTES.FOOTNOTES_VS_ENDNOTES.CHOOSE",
"CMOS.CITATIONS.NOTES.JOURNAL.ARTICLE_ELEMENTS",
"CMOS.CITATIONS.NOTES.LONG_NOTES.PARAGRAPHING",
"CMOS.CITATIONS.NOTES.MULTIPLE_CITATIONS.SINGLE_NOTE",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.HEADINGS_END",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.PLACEMENT_AFTER_PUNCT",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.SEQUENCE_CONTINUOUS",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.SUPERSCRIPT_TEXT",
"CMOS.CITATIONS.NOTES.QUOTE_IN_NOTE.LOCATOR",
"CMOS.CITATIONS.NOTES.SHORT_FORM.BASIC_ELEMENTS",
"CMOS.CITATIONS.NOTES.SHORT_FORM.CROSS_REFERENCE",
"CMOS.CITATIONS.NOTES.SOURCE_NOTES.REPRINTS",
"CMOS.CITATIONS.NOTES.SUBSTANTIVE.SEPARATE_FROM_SOURCE",
"CMOS.CITATIONS.NOTES.UNNUMBERED.NOT_FOR_SOURCES",
"CMOS.CITATIONS.NOTES_BIBLIO.BIBLIOGRAPHY.INCLUDE_WHEN_USED",
"CMOS.CITATIONS.NOTES_BIBLIO.BIBLIOGRAPHY.SORT_BY_AUTHOR",
"CMOS.CITATIONS.NOTES_BIBLIO.FIRST_NOTE.FULL_REFERENCE",
"CMOS.CITATIONS.NOTES_BIBLIO.NAME_ORDER.NOTES_VS_BIBLIO",
"CMOS.CITATIONS.NOTES_BIBLIO.SUBSEQUENT_NOTES.SHORT_FORM",
"CMOS.CITATIONS.ONLINE.ACCESS_DATE.WHEN_NEEDED",
"CMOS.CITATIONS.ONLINE.PERMALINKS.PREFERRED",
"CMOS.CITATIONS.ONLINE.REVISION_DATE.DISTINCT",
"CMOS.CITATIONS.ONLINE.URLS.STABLE",
"CMOS.CITATIONS.ONLINE.VERSION.CITE_RIGHT_VERSION",
"CMOS.CITATIONS.QUOTATIONS.LOCATORS.PAGE_REQUIRED",
"CMOS.CITATIONS.RESEARCH.METADATA.CAPTURE_EARLY",
"CMOS.CITATIONS.SYSTEM.CONSISTENT_CHOICE",
"CMOS.CITATIONS.TITLES.CAPITALIZATION.CONSISTENT"
],
"code": [
"HOUSE.CODE.BLOCKS.LANGUAGE_TAGS.PREFERRED",
"HOUSE.CODE.BLOCKS.NO_CLIPPING",
"HOUSE.CODE.BLOCKS.WRAP_POLICY",
"HOUSE.CODE.INLINE.MONO_BACKTICKS"
],
"headings": [
"BRING.HEADINGS.ALIGNMENT.CONSISTENT_LEVEL",
"BRING.HEADINGS.BLOCK_QUOTE.SPACING_AROUND",
"BRING.HEADINGS.CAPITALIZATION.CONSISTENT",
"BRING.HEADINGS.CONTRAST.CLEAR_HIERARCHY",
"BRING.HEADINGS.DEGRADED.INFER_STRUCTURE",
"BRING.HEADINGS.HIERARCHY.NO_SKIPPED_LEVELS",
"BRING.HEADINGS.MARGIN_HEADS.CLEAR_GUTTER",
"BRING.HEADINGS.PARAGRAPH_INDENT.AFTER_HEAD_NONE",
"BRING.HEADINGS.RELATED_ELEMENTS.COHERENT",
"BRING.HEADINGS.RUN_IN.STANDALONE.CONSISTENT",
"BRING.HEADINGS.SPACING.VERTICAL_RHYTHM",
"BRING.HEADINGS.STRUCTURE.MATCH_TEXT_LOGIC",
"BRING.HEADINGS.STYLE.PALETTE_LIMIT",
"BRING.HEADINGS.SUBHEADS.CROSSHEADS_SIDEHEADS.CHOOSE_DELIBERATELY",
"BRING.HEADINGS.SUBHEADS.LEVELS.AS_MANY_AS_NEEDED",
"BRING.HEADINGS.SUBHEADS.MARGIN_HEADS.RUNNING_SHOULDERHEADS",
"BRING.HEADINGS.SUBHEADS.MIXING.HIERARCHY_PLACEMENT",
"BRING.HEADINGS.SUBHEADS.MIXING_SYMM_ASYMM.AVOID_HAPHAZARD",
"BRING.HEADINGS.SUBHEADS.RIGHT_SIDEHEADS.VISIBILITY",
"BRING.HEADINGS.SUBHEADS.STYLE_CONTRIBUTES_TO_WHOLE",
"BRING.HEADINGS.WEIGHT_SIZE.HIERARCHY_SCALE",
"CMOS.HEADINGS.DIVISIONS.CHAPTERS.MULTIAUTHOR",
"CMOS.HEADINGS.DIVISIONS.LETTERS_DIARIES.HEADINGS",
"CMOS.HEADINGS.LETTERS_DIARIES.DATELINE_FORMAT",
"CMOS.HEADINGS.LETTERS_DIARIES.SIGNATURE_FORMAT",
"CMOS.HEADINGS.MULTIAUTHOR.AUTHOR_ATTRIBUTION_PLACEMENT",
"CMOS.HEADINGS.MULTIAUTHOR.CHAPTER_NUMBERING",
"CMOS.HEADINGS.RUNNING_HEADS.DEFINITION",
"CMOS.HEADINGS.RUNNING_HEADS.DIVISION_MATCH",
"CMOS.HEADINGS.RUNNING_HEADS.LENGTH_SHORT",
"CMOS.HEADINGS.RUNNING_HEADS.NAVIGATION_SCOPE",
"HOUSE.HEADINGS.KEEPS.AVOID_STRANDED"
],
"layout": [
"BRING.LAYOUT.BLOCK_QUOTES.AVOID_CROWDING",
"BRING.LAYOUT.BLOCK_QUOTES.BEFORE_AFTER_SPACING",
"BRING.LAYOUT.BLOCK_QUOTES.EXTRA_LEAD",
"BRING.LAYOUT.BLOCK_QUOTES.INDENT_OR_NARROW",
"BRING.LAYOUT.COLUMNS.BALANCE_LENGTHS",
"BRING.LAYOUT.DEGRADED.HARD_WRAP_REFLOW",
"BRING.LAYOUT.ELEMENT_RELATIONSHIPS.VISIBLE",
"BRING.LAYOUT.FLOATS.PLACEMENT.NEAR_REFERENCE",
"BRING.LAYOUT.GRID.ALIGN_ELEMENTS",
"BRING.LAYOUT.HYPHENATION.AVOID_NEAR_INTERRUPTION",
"BRING.LAYOUT.HYPHENATION.STUB_END_AVOID",
"BRING.LAYOUT.JUSTIFICATION.RAGGED_RIGHT_IF_NEEDED",
"BRING.LAYOUT.LEADING.ADJUST_FOR_SIZE_CHANGES",
"BRING.LAYOUT.LEADING.ALIGN_BASELINE_GRID",
"BRING.LAYOUT.LEADING.AVOID_TOO_LOOSE",
"BRING.LAYOUT.LEADING.AVOID_TOO_TIGHT",
"BRING.LAYOUT.LEADING.CHOOSE_BASE",
"BRING.LAYOUT.LEADING.CONSISTENT_BODY",
"BRING.LAYOUT.LEADING.NEGATIVE.AVOID_CONTINUOUS_TEXT",
"BRING.LAYOUT.LINEBREAKS.AVOID_SAME_WORD_START",
"BRING.LAYOUT.MARGINS.FACING_PAGES.INNER_OUTER",
"BRING.LAYOUT.MEASURE.ADJUST_FOR_TYPE_SIZE",
"BRING.LAYOUT.MEASURE.AVOID_TOO_LONG",
"BRING.LAYOUT.MEASURE.AVOID_TOO_SHORT",
"BRING.LAYOUT.MEASURE.CHANGE_FOR_LISTS",
"BRING.LAYOUT.MEASURE.CODE_BLOCKS.WRAP_POLICY",
"BRING.LAYOUT.MEASURE.COMFORTABLE_RANGE",
"BRING.LAYOUT.MEASURE.CONSISTENT_WITHIN_SECTION",
"BRING.LAYOUT.MEASURE.MULTICOLUMN_TARGETS",
"BRING.LAYOUT.MEASURE.TARGET_RANGE_CHARS",
"BRING.LAYOUT.PAGE.DENSITY.DONT_SUFFOCATE",
"BRING.LAYOUT.PAGE.FRAME.TEXTBLOCK_BALANCE",
"BRING.LAYOUT.PAGINATION.BALANCE_FACING_PAGES",
"BRING.LAYOUT.PAGINATION.ORPHANS_AVOID",
"BRING.LAYOUT.PAGINATION.WIDOWS_AVOID",
"BRING.LAYOUT.PARAGRAPH.BLANK_LINES.SPARING",
"BRING.LAYOUT.PARAGRAPH.INDENT_AFTER_FIRST",
"BRING.LAYOUT.PARAGRAPH.INDENT_OR_SPACE_NOT_BOTH",
"BRING.LAYOUT.PARAGRAPH.INDENT_SIZE.CONSISTENT",
"BRING.LAYOUT.PARAGRAPH.NO_INDENT_AFTER_BLOCKS",
"BRING.LAYOUT.PARAGRAPH.OPENING_FLUSH_LEFT",
"BRING.LAYOUT.RHYTHM.RULES_SERVE_TEXT",
"BRING.LAYOUT.TEXTBLOCK.CONSISTENT_WIDTH",
"BRING.LAYOUT.VERTICAL_RHYTHM.MEASURED_INTERVALS.MULTIPLES",
"HOUSE.LAYOUT.OVERFLOW.OVERFULL_LINES.REPORT",
"HOUSE.LAYOUT.PAGINATION.KEEP_WITH_NEXT.HEADINGS"
],
"links": [
"HOUSE.LINKS.DISALLOW.FILE_URIS",
"HOUSE.LINKS.PUNCTUATION.NO_TRAILING_PUNCT",
"HOUSE.LINKS.TEXT.DESCRIPTIVE",
"HOUSE.LINKS.URLS.PREFER_HTTPS",
"HOUSE.LINKS.WRAP.SAFE_BREAKS"
],
"numbers": [
"CMOS.NUMBERS.BASES.NON_DECIMAL.NO_GROUPING",
"CMOS.NUMBERS.CENTURIES.SPELLED_OUT",
"CMOS.NUMBERS.CONSISTENCY.MIXED_FORMS.AVOID",
"CMOS.NUMBERS.CONTEXTS.ALWAYS_NUMERALS",
"CMOS.NUMBERS.CURRENCY.FORMAT.SYMBOL_PLACEMENT",
"CMOS.NUMBERS.CURRENCY.HISTORICAL.YEAR_CONTEXT",
"CMOS.NUMBERS.CURRENCY.ISO_CODES",
"CMOS.NUMBERS.CURRENCY.LARGE_AMOUNTS",
"CMOS.NUMBERS.CURRENCY.NON_US.DISAMBIGUATE",
"CMOS.NUMBERS.CURRENCY.WORDS_VS_SYMBOLS",
"CMOS.NUMBERS.DATES.ABBREVIATED_YEAR",
"CMOS.NUMBERS.DATES.ALL_NUMERAL",
"CMOS.NUMBERS.DATES.CONSISTENT_FORMAT",
"CMOS.NUMBERS.DATES.ISO_8601",
"CMOS.NUMBERS.DATES.MONTH_DAY_STYLE",
"CMOS.NUMBERS.DATES.YEAR_NUMERALS",
"CMOS.NUMBERS.DECADES.CONSISTENT_FORM",
"CMOS.NUMBERS.DECIMALS.LEADING_ZERO",
"CMOS.NUMBERS.DECIMALS.NUMERALS",
"CMOS.NUMBERS.DECIMAL_MARKER.LOCALE",
"CMOS.NUMBERS.DEGRADED.HARD_WRAP_UNITS",
"CMOS.NUMBERS.DEGRADED.NUMERAL_NORMALIZATION",
"CMOS.NUMBERS.DENSE_CONTEXT.USE_NUMERALS",
"CMOS.NUMBERS.DIGIT_GROUPING.SI_SPACE",
"CMOS.NUMBERS.ERAS.BCE_CE",
"CMOS.NUMBERS.FRACTIONS.MATH.NUMERALS",
"CMOS.NUMBERS.FRACTIONS.MIXED.WHOLE_PLUS_FRACTION",
"CMOS.NUMBERS.FRACTIONS.SIMPLE.SPELL_OUT",
"CMOS.NUMBERS.GROUPING.THOUSANDS_SEPARATOR",
"CMOS.NUMBERS.INCLUSIVE.COMMAS",
"CMOS.NUMBERS.INCLUSIVE.YEARS",
"CMOS.NUMBERS.INCLUSIVE_RANGES.PAGE_NUMBERS.SHORTEN",
"CMOS.NUMBERS.LARGE_VALUES.MILLIONS_BILLIONS",
"CMOS.NUMBERS.LISTS.OUTLINE.NUMERAL_STYLE",
"CMOS.NUMBERS.NAMES.MONARCHS_POPES",
"CMOS.NUMBERS.NUMERALS.MEASUREMENTS.UNITS",
"CMOS.NUMBERS.ORDINALS.SUFFIX.CORRECT",
"CMOS.NUMBERS.PERCENTAGES.NUMERALS",
"CMOS.NUMBERS.PERIODICALS.VOLUME_ISSUE",
"CMOS.NUMBERS.PLACES.BUILDINGS_APTS",
"CMOS.NUMBERS.PLACES.HIGHWAYS",
"CMOS.NUMBERS.PLACES.STREETS",
"CMOS.NUMBERS.PLURALS.DECADE.NO_APOSTROPHE",
"CMOS.NUMBERS.PLURALS.SPELLED_OUT",
"CMOS.NUMBERS.RANGES.EN_DASH.USE",
"CMOS.NUMBERS.RATIOS.FORMAT",
"CMOS.NUMBERS.REFERENCES.PAGE_CHAPTER_FIGURE",
"CMOS.NUMBERS.ROMAN_NUMERALS.USE",
"CMOS.NUMBERS.ROUND_NUMBERS.SPELL_OUT",
"CMOS.NUMBERS.RULE_SELECTION.ALTERNATIVE_ZERO_TO_NINE",
"CMOS.NUMBERS.RULE_SELECTION.GENERAL_OR_ALTERNATIVE",
"CMOS.NUMBERS.SCIENTIFIC.POWERS_OF_TEN",
"CMOS.NUMBERS.SENTENCE_START.AVOID_NUMERAL",
"CMOS.NUMBERS.SI_PREFIXES.NO_HYPHEN",
"CMOS.NUMBERS.SPELLING.ONE_TO_ONE_HUNDRED.DEFAULT",
"CMOS.NUMBERS.TELEPHONE.FORMAT",
"CMOS.NUMBERS.TIME.GENERAL_NUMERALS",
"CMOS.NUMBERS.TIME.ISO_STYLE",
"CMOS.NUMBERS.TIME.NOON_MIDNIGHT",
"CMOS.NUMBERS.TIME.TWENTY_FOUR_HOUR",
"CMOS.NUMBERS.UNITS.REPEATED.OMIT_REPEAT",
"CMOS.NUMBERS.VEHICLES.VESSELS_NUMBERS"
],
"punctuation": [
"CMOS.PUNCTUATION.BLOCK_QUOTES.NO_QUOTE_MARKS",
"CMOS.PUNCTUATION.BRACKETS.NESTED_PARENS",
"CMOS.PUNCTUATION.BRACKETS.TRANSLATED_TEXT",
"CMOS.PUNCTUATION.COLONS.AS_FOLLOWS",
"CMOS.PUNCTUATION.COLONS.CAPITALIZATION",
"CMOS.PUNCTUATION.COLONS.INTRO_QUOTE_QUESTION",
"CMOS.PUNCTUATION.COLONS.SPACE_AFTER",
"CMOS.PUNCTUATION.COMMAS.ADDRESSES",
"CMOS.PUNCTUATION.COMMAS.APPOSITIVES",
"CMOS.PUNCTUATION.COMMAS.COMPOUND_PREDICATES",
"CMOS.PUNCTUATION.COMMAS.DATES",
"CMOS.PUNCTUATION.COMMAS.DEPENDENT_CLAUSE_AFTER",
"CMOS.PUNCTUATION.COMMAS.DESCRIPTIVE_PHRASES",
"CMOS.PUNCTUATION.COMMAS.INTRODUCTORY_ELEMENTS.CLARITY",
"CMOS.PUNCTUATION.COMMAS.INTRODUCTORY_PHRASES",
"CMOS.PUNCTUATION.COMMAS.NONRESTRICTIVE.SET_OFF",
"CMOS.PUNCTUATION.COMMAS.PARTICIPIAL_PHRASES",
"CMOS.PUNCTUATION.COMMAS.QUESTIONS",
"CMOS.PUNCTUATION.COMMAS.QUOTED_TITLES",
"CMOS.PUNCTUATION.COMMAS.REPEATED_ADJECTIVES",
"CMOS.PUNCTUATION.COMMAS.RESTRICTIVE.NO_SET_OFF",
"CMOS.PUNCTUATION.COMMAS.SERIAL_COMMA.DEFAULT",
"CMOS.PUNCTUATION.DASHES.EM_DASH.USE_WITHOUT_SPACES_US",
"CMOS.PUNCTUATION.DASHES.EM_INSTEAD_OF_QUOTES",
"CMOS.PUNCTUATION.DASHES.EM_LINE_BREAKS",
"CMOS.PUNCTUATION.DASHES.EN_DASH.RANGES",
"CMOS.PUNCTUATION.DEGRADED.EXTRA_SPACES.AFTER_PUNCT",
"CMOS.PUNCTUATION.ELLIPSIS.FORMAT.CONSISTENT",
"CMOS.PUNCTUATION.EXCLAMATION.USE_SPARINGLY",
"CMOS.PUNCTUATION.EXCLAMATION.VS_QUESTION",
"CMOS.PUNCTUATION.HYPHENATION.COMPOUND_DEFINITION",
"CMOS.PUNCTUATION.HYPHENATION.GENERAL_CHOICE",
"CMOS.PUNCTUATION.HYPHENATION.READABILITY",
"CMOS.PUNCTUATION.HYPHENATION.SUSPENDED",
"CMOS.PUNCTUATION.HYPHENATION.TREND_CLOSED",
"CMOS.PUNCTUATION.HYPHENS.ADVERB_LY.NO_HYPHEN",
"CMOS.PUNCTUATION.HYPHENS.COMPOUND_MODIFIERS.BEFORE_NOUN",
"CMOS.PUNCTUATION.MULTIPLE_MARKS.AVOID_STACKING",
"CMOS.PUNCTUATION.PARENS.GLOSSES_TRANSLATIONS",
"CMOS.PUNCTUATION.PARENS.NESTING",
"CMOS.PUNCTUATION.PARENS.USE",
"CMOS.PUNCTUATION.PARENS_BRACKETS.BALANCE",
"CMOS.PUNCTUATION.PERIODS.USE",
"CMOS.PUNCTUATION.PERIODS.WITH_PARENS",
"CMOS.PUNCTUATION.QUESTION_MARK.DIRECT_VS_INDIRECT",
"CMOS.PUNCTUATION.QUESTION_MARK.USE",
"CMOS.PUNCTUATION.QUESTION_MARK.WITH_PUNCT",
"CMOS.PUNCTUATION.QUOTATION_MARKS.DOUBLE_PRIMARY_US",
"CMOS.PUNCTUATION.QUOTATION_MARKS.PUNCTUATION_PLACEMENT_US",
"CMOS.PUNCTUATION.QUOTES.SMART_QUOTES",
"CMOS.PUNCTUATION.SEMICOLONS.BEFORE_CONJUNCTION",
"CMOS.PUNCTUATION.SEMICOLONS.COMPLEX_SERIES.SEPARATE",
"CMOS.PUNCTUATION.SEMICOLONS.CONJUNCTIVE_PHRASES",
"CMOS.PUNCTUATION.SLASHES.ALTERNATIVES",
"CMOS.PUNCTUATION.SLASHES.TWO_YEAR_SPANS"
],
"tables": [
"BRING.TABLES.CAPTIONS.CLEAR",
"BRING.TABLES.COLUMN_ALIGNMENT.CONSISTENT",
"BRING.TABLES.DATA_TYPES.NOT_MIXED",
"BRING.TABLES.DEGRADED.REBUILD_COLUMNS",
"BRING.TABLES.EDIT_AS_TEXT.READABILITY",
"BRING.TABLES.FURNITURE.MINIMIZE",
"BRING.TABLES.GROUPING.WHITESPACE",
"BRING.TABLES.GUIDES.READING_DIRECTION",
"BRING.TABLES.HEADERS.ALIGN_WITH_COLUMNS",
"BRING.TABLES.HEADERS.CONCISE",
"BRING.TABLES.MULTI_LINE_HEADERS.AVOID",
"BRING.TABLES.NUMERIC_PRECISION.CONSISTENT",
"BRING.TABLES.ORDER.LOGICAL",
"BRING.TABLES.ROW_SPACING.READABLE",
"BRING.TABLES.SOURCES.NOTES.DISTINCT",
"BRING.TABLES.STUB_COLUMN.USE",
"BRING.TABLES.TEXT_ORIENTATION.HORIZONTAL",
"BRING.TABLES.TITLES.CONSISTENT_PLACEMENT",
"BRING.TABLES.TYPE_SIZE.READABLE",
"BRING.TABLES.UNITS.IN_HEADERS",
"HOUSE.TABLES.ALIGNMENT.DECIMALS",
"HOUSE.TABLES.HEADERS.REPEAT_ON_PAGEBREAK",
"HOUSE.TABLES.OVERFLOW.NO_CLIPPING"
],
"typography": [
"BRING.TYPOGRAPHY.ALIGNMENT.DONT_STRETCH_SPACE",
"BRING.TYPOGRAPHY.HYPHENATION.AVOID_AFTER_SHORT_LINE",
"BRING.TYPOGRAPHY.HYPHENATION.AVOID_PROPER_NAMES",
"BRING.TYPOGRAPHY.HYPHENATION.HARD_SPACES.SHORT_EXPRESSIONS",
"BRING.TYPOGRAPHY.HYPHENATION.LANGUAGE_DICTIONARY.MATCH",
"BRING.TYPOGRAPHY.HYPHENATION.MAX_CONSECUTIVE_LINES",
"BRING.TYPOGRAPHY.HYPHENATION.MIN_LEFT_RIGHT",
"BRING.TYPOGRAPHY.KERNING.CONSISTENT_OR_NONE",
"BRING.TYPOGRAPHY.LETTERFORMS.AVOID_DISTORTION",
"BRING.TYPOGRAPHY.NUMBER_STRINGS.SPACE_FOR_READABILITY",
"BRING.TYPOGRAPHY.SPACING.INITIALS.THIN_SPACE",
"BRING.TYPOGRAPHY.SPACING.SENTENCE_SPACE.SINGLE",
"BRING.TYPOGRAPHY.SPACING.WORD_SPACE.DEFINE",
"BRING.TYPOGRAPHY.TRACKING.CAPS_SMALLCAPS_AND_LONG_DIGITS",
"BRING.TYPOGRAPHY.TRACKING.LOWERCASE.AVOID"
]
}

View file

@ -0,0 +1,317 @@
{
"lint": [
"BRING.HEADINGS.CAPITALIZATION.CONSISTENT",
"BRING.HEADINGS.DEGRADED.INFER_STRUCTURE",
"BRING.HEADINGS.HIERARCHY.NO_SKIPPED_LEVELS",
"BRING.HEADINGS.SUBHEADS.LEVELS.AS_MANY_AS_NEEDED",
"BRING.LAYOUT.DEGRADED.HARD_WRAP_REFLOW",
"BRING.TABLES.DEGRADED.REBUILD_COLUMNS",
"BRING.TYPOGRAPHY.ALIGNMENT.DONT_STRETCH_SPACE",
"BRING.TYPOGRAPHY.HYPHENATION.HARD_SPACES.SHORT_EXPRESSIONS",
"BRING.TYPOGRAPHY.SPACING.SENTENCE_SPACE.SINGLE",
"CMOS.CITATIONS.DEGRADED.NOTE_MARKERS.REPAIR",
"CMOS.CITATIONS.DEGRADED.URL_LINEBREAKS.NORMALIZE",
"CMOS.CITATIONS.DOI.PREFERRED_OVER_URL",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.HEADINGS_END",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.PLACEMENT_AFTER_PUNCT",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.SEQUENCE_CONTINUOUS",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.SUPERSCRIPT_TEXT",
"CMOS.NUMBERS.DECIMALS.LEADING_ZERO",
"CMOS.NUMBERS.DECIMALS.NUMERALS",
"CMOS.NUMBERS.DECIMAL_MARKER.LOCALE",
"CMOS.NUMBERS.DEGRADED.HARD_WRAP_UNITS",
"CMOS.NUMBERS.DEGRADED.NUMERAL_NORMALIZATION",
"CMOS.NUMBERS.GROUPING.THOUSANDS_SEPARATOR",
"CMOS.NUMBERS.ORDINALS.SUFFIX.CORRECT",
"CMOS.NUMBERS.PERCENTAGES.NUMERALS",
"CMOS.NUMBERS.PLURALS.DECADE.NO_APOSTROPHE",
"CMOS.NUMBERS.RANGES.EN_DASH.USE",
"CMOS.NUMBERS.SI_PREFIXES.NO_HYPHEN",
"CMOS.PUNCTUATION.BLOCK_QUOTES.NO_QUOTE_MARKS",
"CMOS.PUNCTUATION.COLONS.SPACE_AFTER",
"CMOS.PUNCTUATION.COMMAS.SERIAL_COMMA.DEFAULT",
"CMOS.PUNCTUATION.DASHES.EM_DASH.USE_WITHOUT_SPACES_US",
"CMOS.PUNCTUATION.DASHES.EN_DASH.RANGES",
"CMOS.PUNCTUATION.DEGRADED.EXTRA_SPACES.AFTER_PUNCT",
"CMOS.PUNCTUATION.ELLIPSIS.FORMAT.CONSISTENT",
"CMOS.PUNCTUATION.MULTIPLE_MARKS.AVOID_STACKING",
"CMOS.PUNCTUATION.PARENS_BRACKETS.BALANCE",
"CMOS.PUNCTUATION.QUOTES.SMART_QUOTES",
"HOUSE.A11Y.HEADINGS.NO_SKIPS",
"HOUSE.A11Y.IMAGES.ALT_REQUIRED",
"HOUSE.A11Y.LINK_TEXT.DESCRIPTIVE",
"HOUSE.CODE.BLOCKS.LANGUAGE_TAGS.PREFERRED",
"HOUSE.CODE.INLINE.MONO_BACKTICKS",
"HOUSE.LINKS.DISALLOW.FILE_URIS",
"HOUSE.LINKS.PUNCTUATION.NO_TRAILING_PUNCT",
"HOUSE.LINKS.TEXT.DESCRIPTIVE",
"HOUSE.LINKS.URLS.PREFER_HTTPS"
],
"manual": [
"BRING.HEADINGS.CONTRAST.CLEAR_HIERARCHY",
"BRING.HEADINGS.RELATED_ELEMENTS.COHERENT",
"BRING.HEADINGS.RUN_IN.STANDALONE.CONSISTENT",
"BRING.HEADINGS.STRUCTURE.MATCH_TEXT_LOGIC",
"BRING.HEADINGS.STYLE.PALETTE_LIMIT",
"BRING.HEADINGS.SUBHEADS.MIXING.HIERARCHY_PLACEMENT",
"BRING.LAYOUT.ELEMENT_RELATIONSHIPS.VISIBLE",
"BRING.LAYOUT.FLOATS.PLACEMENT.NEAR_REFERENCE",
"BRING.LAYOUT.MEASURE.CODE_BLOCKS.WRAP_POLICY",
"BRING.LAYOUT.PAGE.DENSITY.DONT_SUFFOCATE",
"BRING.LAYOUT.PARAGRAPH.BLANK_LINES.SPARING",
"BRING.LAYOUT.RHYTHM.RULES_SERVE_TEXT",
"BRING.TABLES.CAPTIONS.CLEAR",
"BRING.TABLES.DATA_TYPES.NOT_MIXED",
"BRING.TABLES.EDIT_AS_TEXT.READABILITY",
"BRING.TABLES.GUIDES.READING_DIRECTION",
"BRING.TABLES.HEADERS.CONCISE",
"BRING.TABLES.MULTI_LINE_HEADERS.AVOID",
"BRING.TABLES.NUMERIC_PRECISION.CONSISTENT",
"BRING.TABLES.ORDER.LOGICAL",
"BRING.TABLES.SOURCES.NOTES.DISTINCT",
"BRING.TABLES.STUB_COLUMN.USE",
"BRING.TABLES.TEXT_ORIENTATION.HORIZONTAL",
"BRING.TABLES.TITLES.CONSISTENT_PLACEMENT",
"BRING.TABLES.UNITS.IN_HEADERS",
"BRING.TYPOGRAPHY.HYPHENATION.AVOID_PROPER_NAMES",
"BRING.TYPOGRAPHY.KERNING.CONSISTENT_OR_NONE",
"BRING.TYPOGRAPHY.NUMBER_STRINGS.SPACE_FOR_READABILITY",
"BRING.TYPOGRAPHY.SPACING.INITIALS.THIN_SPACE",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.DIRECT_QUOTES",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.EXTRA_INFO",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.LOCATORS",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.MULTI_SOURCES",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.PARENTHETICAL",
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.PLACEMENT",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.ALPHABETICAL",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.ORDER_AND_YEAR",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.REPEATED_NAMES",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.REQUIRED",
"CMOS.CITATIONS.BIBLIOGRAPHY.ALT_NAMES.CROSSREF",
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.CONSISTENT_FORM",
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.INITIALS_PREFERRED",
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.PUBLISHED_FORM",
"CMOS.CITATIONS.BIBLIOGRAPHY.MULTI_AUTHORS.ORDER",
"CMOS.CITATIONS.BIBLIOGRAPHY.NO_AUTHOR.TITLE_LEAD",
"CMOS.CITATIONS.BIBLIOGRAPHY.PSEUDONYMS.CONSISTENT",
"CMOS.CITATIONS.BIBLIOGRAPHY.REPEATED_NAMES.THREE_EM_DASH",
"CMOS.CITATIONS.BIBLIOGRAPHY.SAME_AUTHOR.ORDER",
"CMOS.CITATIONS.BIBLIOGRAPHY.SAME_SURNAME.DISAMBIGUATE",
"CMOS.CITATIONS.IBID.MINIMIZE_OR_AVOID",
"CMOS.CITATIONS.LEGAL_PUBLIC_DOCS.USE_JURISDICTIONAL_FORMAT",
"CMOS.CITATIONS.MATCHING.BIBLIO_ENTRY_REQUIRED",
"CMOS.CITATIONS.NOTES.AUTHOR_DATE_PLUS_NOTES",
"CMOS.CITATIONS.NOTES.AVOID_OVERLONG",
"CMOS.CITATIONS.NOTES.CHAPTER_IN_EDITED_BOOK",
"CMOS.CITATIONS.NOTES.ENDNOTES.AVOID_IBID",
"CMOS.CITATIONS.NOTES.ENDNOTES.PLACEMENT",
"CMOS.CITATIONS.NOTES.ENDNOTES.RUNNING_HEADS",
"CMOS.CITATIONS.NOTES.FOOTNOTES_VS_ENDNOTES.CHOOSE",
"CMOS.CITATIONS.NOTES.JOURNAL.ARTICLE_ELEMENTS",
"CMOS.CITATIONS.NOTES.LONG_NOTES.PARAGRAPHING",
"CMOS.CITATIONS.NOTES.MULTIPLE_CITATIONS.SINGLE_NOTE",
"CMOS.CITATIONS.NOTES.QUOTE_IN_NOTE.LOCATOR",
"CMOS.CITATIONS.NOTES.SHORT_FORM.BASIC_ELEMENTS",
"CMOS.CITATIONS.NOTES.SHORT_FORM.CROSS_REFERENCE",
"CMOS.CITATIONS.NOTES.SOURCE_NOTES.REPRINTS",
"CMOS.CITATIONS.NOTES.SUBSTANTIVE.SEPARATE_FROM_SOURCE",
"CMOS.CITATIONS.NOTES.UNNUMBERED.NOT_FOR_SOURCES",
"CMOS.CITATIONS.NOTES_BIBLIO.BIBLIOGRAPHY.INCLUDE_WHEN_USED",
"CMOS.CITATIONS.NOTES_BIBLIO.BIBLIOGRAPHY.SORT_BY_AUTHOR",
"CMOS.CITATIONS.NOTES_BIBLIO.FIRST_NOTE.FULL_REFERENCE",
"CMOS.CITATIONS.NOTES_BIBLIO.NAME_ORDER.NOTES_VS_BIBLIO",
"CMOS.CITATIONS.NOTES_BIBLIO.SUBSEQUENT_NOTES.SHORT_FORM",
"CMOS.CITATIONS.ONLINE.ACCESS_DATE.WHEN_NEEDED",
"CMOS.CITATIONS.ONLINE.PERMALINKS.PREFERRED",
"CMOS.CITATIONS.ONLINE.REVISION_DATE.DISTINCT",
"CMOS.CITATIONS.ONLINE.URLS.STABLE",
"CMOS.CITATIONS.ONLINE.VERSION.CITE_RIGHT_VERSION",
"CMOS.CITATIONS.QUOTATIONS.LOCATORS.PAGE_REQUIRED",
"CMOS.CITATIONS.RESEARCH.METADATA.CAPTURE_EARLY",
"CMOS.CITATIONS.SYSTEM.CONSISTENT_CHOICE",
"CMOS.CITATIONS.TITLES.CAPITALIZATION.CONSISTENT",
"CMOS.HEADINGS.DIVISIONS.CHAPTERS.MULTIAUTHOR",
"CMOS.HEADINGS.DIVISIONS.LETTERS_DIARIES.HEADINGS",
"CMOS.HEADINGS.LETTERS_DIARIES.DATELINE_FORMAT",
"CMOS.HEADINGS.LETTERS_DIARIES.SIGNATURE_FORMAT",
"CMOS.HEADINGS.MULTIAUTHOR.AUTHOR_ATTRIBUTION_PLACEMENT",
"CMOS.HEADINGS.MULTIAUTHOR.CHAPTER_NUMBERING",
"CMOS.HEADINGS.RUNNING_HEADS.DEFINITION",
"CMOS.HEADINGS.RUNNING_HEADS.DIVISION_MATCH",
"CMOS.HEADINGS.RUNNING_HEADS.LENGTH_SHORT",
"CMOS.HEADINGS.RUNNING_HEADS.NAVIGATION_SCOPE",
"CMOS.NUMBERS.BASES.NON_DECIMAL.NO_GROUPING",
"CMOS.NUMBERS.CENTURIES.SPELLED_OUT",
"CMOS.NUMBERS.CONSISTENCY.MIXED_FORMS.AVOID",
"CMOS.NUMBERS.CONTEXTS.ALWAYS_NUMERALS",
"CMOS.NUMBERS.CURRENCY.FORMAT.SYMBOL_PLACEMENT",
"CMOS.NUMBERS.CURRENCY.HISTORICAL.YEAR_CONTEXT",
"CMOS.NUMBERS.CURRENCY.ISO_CODES",
"CMOS.NUMBERS.CURRENCY.LARGE_AMOUNTS",
"CMOS.NUMBERS.CURRENCY.NON_US.DISAMBIGUATE",
"CMOS.NUMBERS.CURRENCY.WORDS_VS_SYMBOLS",
"CMOS.NUMBERS.DATES.ABBREVIATED_YEAR",
"CMOS.NUMBERS.DATES.ALL_NUMERAL",
"CMOS.NUMBERS.DATES.CONSISTENT_FORMAT",
"CMOS.NUMBERS.DATES.ISO_8601",
"CMOS.NUMBERS.DATES.MONTH_DAY_STYLE",
"CMOS.NUMBERS.DATES.YEAR_NUMERALS",
"CMOS.NUMBERS.DECADES.CONSISTENT_FORM",
"CMOS.NUMBERS.DENSE_CONTEXT.USE_NUMERALS",
"CMOS.NUMBERS.ERAS.BCE_CE",
"CMOS.NUMBERS.FRACTIONS.MATH.NUMERALS",
"CMOS.NUMBERS.FRACTIONS.MIXED.WHOLE_PLUS_FRACTION",
"CMOS.NUMBERS.FRACTIONS.SIMPLE.SPELL_OUT",
"CMOS.NUMBERS.INCLUSIVE.COMMAS",
"CMOS.NUMBERS.INCLUSIVE.YEARS",
"CMOS.NUMBERS.INCLUSIVE_RANGES.PAGE_NUMBERS.SHORTEN",
"CMOS.NUMBERS.LARGE_VALUES.MILLIONS_BILLIONS",
"CMOS.NUMBERS.LISTS.OUTLINE.NUMERAL_STYLE",
"CMOS.NUMBERS.NAMES.MONARCHS_POPES",
"CMOS.NUMBERS.NUMERALS.MEASUREMENTS.UNITS",
"CMOS.NUMBERS.PERIODICALS.VOLUME_ISSUE",
"CMOS.NUMBERS.PLACES.BUILDINGS_APTS",
"CMOS.NUMBERS.PLACES.HIGHWAYS",
"CMOS.NUMBERS.PLACES.STREETS",
"CMOS.NUMBERS.PLURALS.SPELLED_OUT",
"CMOS.NUMBERS.RATIOS.FORMAT",
"CMOS.NUMBERS.REFERENCES.PAGE_CHAPTER_FIGURE",
"CMOS.NUMBERS.ROMAN_NUMERALS.USE",
"CMOS.NUMBERS.ROUND_NUMBERS.SPELL_OUT",
"CMOS.NUMBERS.RULE_SELECTION.ALTERNATIVE_ZERO_TO_NINE",
"CMOS.NUMBERS.RULE_SELECTION.GENERAL_OR_ALTERNATIVE",
"CMOS.NUMBERS.SCIENTIFIC.POWERS_OF_TEN",
"CMOS.NUMBERS.SENTENCE_START.AVOID_NUMERAL",
"CMOS.NUMBERS.SPELLING.ONE_TO_ONE_HUNDRED.DEFAULT",
"CMOS.NUMBERS.TELEPHONE.FORMAT",
"CMOS.NUMBERS.TIME.GENERAL_NUMERALS",
"CMOS.NUMBERS.TIME.ISO_STYLE",
"CMOS.NUMBERS.TIME.NOON_MIDNIGHT",
"CMOS.NUMBERS.TIME.TWENTY_FOUR_HOUR",
"CMOS.NUMBERS.UNITS.REPEATED.OMIT_REPEAT",
"CMOS.NUMBERS.VEHICLES.VESSELS_NUMBERS",
"CMOS.PUNCTUATION.BRACKETS.NESTED_PARENS",
"CMOS.PUNCTUATION.BRACKETS.TRANSLATED_TEXT",
"CMOS.PUNCTUATION.COLONS.AS_FOLLOWS",
"CMOS.PUNCTUATION.COLONS.CAPITALIZATION",
"CMOS.PUNCTUATION.COLONS.INTRO_QUOTE_QUESTION",
"CMOS.PUNCTUATION.COMMAS.ADDRESSES",
"CMOS.PUNCTUATION.COMMAS.APPOSITIVES",
"CMOS.PUNCTUATION.COMMAS.COMPOUND_PREDICATES",
"CMOS.PUNCTUATION.COMMAS.DATES",
"CMOS.PUNCTUATION.COMMAS.DEPENDENT_CLAUSE_AFTER",
"CMOS.PUNCTUATION.COMMAS.DESCRIPTIVE_PHRASES",
"CMOS.PUNCTUATION.COMMAS.INTRODUCTORY_ELEMENTS.CLARITY",
"CMOS.PUNCTUATION.COMMAS.INTRODUCTORY_PHRASES",
"CMOS.PUNCTUATION.COMMAS.NONRESTRICTIVE.SET_OFF",
"CMOS.PUNCTUATION.COMMAS.PARTICIPIAL_PHRASES",
"CMOS.PUNCTUATION.COMMAS.QUESTIONS",
"CMOS.PUNCTUATION.COMMAS.QUOTED_TITLES",
"CMOS.PUNCTUATION.COMMAS.REPEATED_ADJECTIVES",
"CMOS.PUNCTUATION.COMMAS.RESTRICTIVE.NO_SET_OFF",
"CMOS.PUNCTUATION.DASHES.EM_INSTEAD_OF_QUOTES",
"CMOS.PUNCTUATION.EXCLAMATION.USE_SPARINGLY",
"CMOS.PUNCTUATION.EXCLAMATION.VS_QUESTION",
"CMOS.PUNCTUATION.HYPHENATION.COMPOUND_DEFINITION",
"CMOS.PUNCTUATION.HYPHENATION.GENERAL_CHOICE",
"CMOS.PUNCTUATION.HYPHENATION.READABILITY",
"CMOS.PUNCTUATION.HYPHENATION.SUSPENDED",
"CMOS.PUNCTUATION.HYPHENATION.TREND_CLOSED",
"CMOS.PUNCTUATION.HYPHENS.ADVERB_LY.NO_HYPHEN",
"CMOS.PUNCTUATION.HYPHENS.COMPOUND_MODIFIERS.BEFORE_NOUN",
"CMOS.PUNCTUATION.PARENS.GLOSSES_TRANSLATIONS",
"CMOS.PUNCTUATION.PARENS.NESTING",
"CMOS.PUNCTUATION.PARENS.USE",
"CMOS.PUNCTUATION.PERIODS.USE",
"CMOS.PUNCTUATION.PERIODS.WITH_PARENS",
"CMOS.PUNCTUATION.QUESTION_MARK.DIRECT_VS_INDIRECT",
"CMOS.PUNCTUATION.QUESTION_MARK.USE",
"CMOS.PUNCTUATION.QUESTION_MARK.WITH_PUNCT",
"CMOS.PUNCTUATION.QUOTATION_MARKS.DOUBLE_PRIMARY_US",
"CMOS.PUNCTUATION.QUOTATION_MARKS.PUNCTUATION_PLACEMENT_US",
"CMOS.PUNCTUATION.SEMICOLONS.BEFORE_CONJUNCTION",
"CMOS.PUNCTUATION.SEMICOLONS.COMPLEX_SERIES.SEPARATE",
"CMOS.PUNCTUATION.SEMICOLONS.CONJUNCTIVE_PHRASES",
"CMOS.PUNCTUATION.SLASHES.ALTERNATIVES",
"CMOS.PUNCTUATION.SLASHES.TWO_YEAR_SPANS"
],
"postrender": [
"BRING.LAYOUT.COLUMNS.BALANCE_LENGTHS",
"BRING.LAYOUT.HYPHENATION.AVOID_NEAR_INTERRUPTION",
"BRING.LAYOUT.LINEBREAKS.AVOID_SAME_WORD_START",
"BRING.LAYOUT.PAGINATION.BALANCE_FACING_PAGES",
"BRING.LAYOUT.PAGINATION.ORPHANS_AVOID",
"BRING.LAYOUT.PAGINATION.WIDOWS_AVOID",
"BRING.TYPOGRAPHY.HYPHENATION.AVOID_AFTER_SHORT_LINE",
"CMOS.CITATIONS.NOTES.FOOTNOTES.PAGE_BREAKS",
"HOUSE.CODE.BLOCKS.NO_CLIPPING",
"HOUSE.HEADINGS.KEEPS.AVOID_STRANDED",
"HOUSE.LAYOUT.OVERFLOW.OVERFULL_LINES.REPORT",
"HOUSE.LAYOUT.PAGINATION.KEEP_WITH_NEXT.HEADINGS",
"HOUSE.TABLES.OVERFLOW.NO_CLIPPING"
],
"typeset": [
"BRING.HEADINGS.ALIGNMENT.CONSISTENT_LEVEL",
"BRING.HEADINGS.BLOCK_QUOTE.SPACING_AROUND",
"BRING.HEADINGS.MARGIN_HEADS.CLEAR_GUTTER",
"BRING.HEADINGS.PARAGRAPH_INDENT.AFTER_HEAD_NONE",
"BRING.HEADINGS.SPACING.VERTICAL_RHYTHM",
"BRING.HEADINGS.SUBHEADS.CROSSHEADS_SIDEHEADS.CHOOSE_DELIBERATELY",
"BRING.HEADINGS.SUBHEADS.MARGIN_HEADS.RUNNING_SHOULDERHEADS",
"BRING.HEADINGS.SUBHEADS.MIXING_SYMM_ASYMM.AVOID_HAPHAZARD",
"BRING.HEADINGS.SUBHEADS.RIGHT_SIDEHEADS.VISIBILITY",
"BRING.HEADINGS.SUBHEADS.STYLE_CONTRIBUTES_TO_WHOLE",
"BRING.HEADINGS.WEIGHT_SIZE.HIERARCHY_SCALE",
"BRING.LAYOUT.BLOCK_QUOTES.AVOID_CROWDING",
"BRING.LAYOUT.BLOCK_QUOTES.BEFORE_AFTER_SPACING",
"BRING.LAYOUT.BLOCK_QUOTES.EXTRA_LEAD",
"BRING.LAYOUT.BLOCK_QUOTES.INDENT_OR_NARROW",
"BRING.LAYOUT.GRID.ALIGN_ELEMENTS",
"BRING.LAYOUT.HYPHENATION.STUB_END_AVOID",
"BRING.LAYOUT.JUSTIFICATION.RAGGED_RIGHT_IF_NEEDED",
"BRING.LAYOUT.LEADING.ADJUST_FOR_SIZE_CHANGES",
"BRING.LAYOUT.LEADING.ALIGN_BASELINE_GRID",
"BRING.LAYOUT.LEADING.AVOID_TOO_LOOSE",
"BRING.LAYOUT.LEADING.AVOID_TOO_TIGHT",
"BRING.LAYOUT.LEADING.CHOOSE_BASE",
"BRING.LAYOUT.LEADING.CONSISTENT_BODY",
"BRING.LAYOUT.LEADING.NEGATIVE.AVOID_CONTINUOUS_TEXT",
"BRING.LAYOUT.MARGINS.FACING_PAGES.INNER_OUTER",
"BRING.LAYOUT.MEASURE.ADJUST_FOR_TYPE_SIZE",
"BRING.LAYOUT.MEASURE.AVOID_TOO_LONG",
"BRING.LAYOUT.MEASURE.AVOID_TOO_SHORT",
"BRING.LAYOUT.MEASURE.CHANGE_FOR_LISTS",
"BRING.LAYOUT.MEASURE.COMFORTABLE_RANGE",
"BRING.LAYOUT.MEASURE.CONSISTENT_WITHIN_SECTION",
"BRING.LAYOUT.MEASURE.MULTICOLUMN_TARGETS",
"BRING.LAYOUT.MEASURE.TARGET_RANGE_CHARS",
"BRING.LAYOUT.PAGE.FRAME.TEXTBLOCK_BALANCE",
"BRING.LAYOUT.PARAGRAPH.INDENT_AFTER_FIRST",
"BRING.LAYOUT.PARAGRAPH.INDENT_OR_SPACE_NOT_BOTH",
"BRING.LAYOUT.PARAGRAPH.INDENT_SIZE.CONSISTENT",
"BRING.LAYOUT.PARAGRAPH.NO_INDENT_AFTER_BLOCKS",
"BRING.LAYOUT.PARAGRAPH.OPENING_FLUSH_LEFT",
"BRING.LAYOUT.TEXTBLOCK.CONSISTENT_WIDTH",
"BRING.LAYOUT.VERTICAL_RHYTHM.MEASURED_INTERVALS.MULTIPLES",
"BRING.TABLES.COLUMN_ALIGNMENT.CONSISTENT",
"BRING.TABLES.FURNITURE.MINIMIZE",
"BRING.TABLES.GROUPING.WHITESPACE",
"BRING.TABLES.HEADERS.ALIGN_WITH_COLUMNS",
"BRING.TABLES.ROW_SPACING.READABLE",
"BRING.TABLES.TYPE_SIZE.READABLE",
"BRING.TYPOGRAPHY.HYPHENATION.LANGUAGE_DICTIONARY.MATCH",
"BRING.TYPOGRAPHY.HYPHENATION.MAX_CONSECUTIVE_LINES",
"BRING.TYPOGRAPHY.HYPHENATION.MIN_LEFT_RIGHT",
"BRING.TYPOGRAPHY.LETTERFORMS.AVOID_DISTORTION",
"BRING.TYPOGRAPHY.SPACING.WORD_SPACE.DEFINE",
"BRING.TYPOGRAPHY.TRACKING.CAPS_SMALLCAPS_AND_LONG_DIGITS",
"BRING.TYPOGRAPHY.TRACKING.LOWERCASE.AVOID",
"CMOS.NUMBERS.DIGIT_GROUPING.SI_SPACE",
"CMOS.PUNCTUATION.DASHES.EM_LINE_BREAKS",
"HOUSE.A11Y.DOCUMENT_LANGUAGE.DECLARE",
"HOUSE.CODE.BLOCKS.WRAP_POLICY",
"HOUSE.LINKS.WRAP.SAFE_BREAKS",
"HOUSE.TABLES.ALIGNMENT.DECIMALS",
"HOUSE.TABLES.HEADERS.REPEAT_ON_PAGEBREAK"
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,732 @@
{
"BRING \u00a71.2.2 p20 (scan p19)": [
"BRING.HEADINGS.DEGRADED.INFER_STRUCTURE",
"BRING.HEADINGS.STRUCTURE.MATCH_TEXT_LOGIC"
],
"BRING \u00a71.2.3 p21 (scan p20)": [
"BRING.HEADINGS.RELATED_ELEMENTS.COHERENT"
],
"BRING \u00a71.2.5 p23 (scan p22)": [
"BRING.LAYOUT.ELEMENT_RELATIONSHIPS.VISIBLE",
"BRING.LAYOUT.FLOATS.PLACEMENT.NEAR_REFERENCE",
"BRING.LAYOUT.GRID.ALIGN_ELEMENTS",
"BRING.LAYOUT.MARGINS.FACING_PAGES.INNER_OUTER",
"BRING.LAYOUT.PAGE.FRAME.TEXTBLOCK_BALANCE",
"BRING.LAYOUT.TEXTBLOCK.CONSISTENT_WIDTH"
],
"BRING \u00a72-4.1 p42": [
"BRING.LAYOUT.RHYTHM.RULES_SERVE_TEXT"
],
"BRING \u00a72-4.10 p44": [
"BRING.LAYOUT.PAGINATION.BALANCE_FACING_PAGES"
],
"BRING \u00a72-4.11 p44": [
"BRING.LAYOUT.HYPHENATION.AVOID_NEAR_INTERRUPTION"
],
"BRING \u00a72-4.2 p42": [
"BRING.TYPOGRAPHY.HYPHENATION.MIN_LEFT_RIGHT"
],
"BRING \u00a72-4.3 p42": [
"BRING.LAYOUT.HYPHENATION.STUB_END_AVOID"
],
"BRING \u00a72-4.4 p43": [
"BRING.TYPOGRAPHY.HYPHENATION.MAX_CONSECUTIVE_LINES"
],
"BRING \u00a72-4.5 p43": [
"BRING.TYPOGRAPHY.HYPHENATION.AVOID_PROPER_NAMES"
],
"BRING \u00a72-4.6 p43": [
"BRING.TYPOGRAPHY.HYPHENATION.LANGUAGE_DICTIONARY.MATCH"
],
"BRING \u00a72-4.7 p43": [
"BRING.TYPOGRAPHY.HYPHENATION.HARD_SPACES.SHORT_EXPRESSIONS"
],
"BRING \u00a72-4.8 p43": [
"BRING.LAYOUT.LINEBREAKS.AVOID_SAME_WORD_START",
"BRING.TYPOGRAPHY.HYPHENATION.AVOID_AFTER_SHORT_LINE"
],
"BRING \u00a72-4.9 p44": [
"BRING.LAYOUT.PAGINATION.ORPHANS_AVOID",
"BRING.LAYOUT.PAGINATION.WIDOWS_AVOID"
],
"BRING \u00a72.1.1 p25": [
"BRING.TYPOGRAPHY.SPACING.WORD_SPACE.DEFINE"
],
"BRING \u00a72.1.10 p35 (scan p34)": [
"BRING.TYPOGRAPHY.ALIGNMENT.DONT_STRETCH_SPACE"
],
"BRING \u00a72.1.2 p26": [
"BRING.LAYOUT.MEASURE.COMFORTABLE_RANGE",
"BRING.LAYOUT.MEASURE.MULTICOLUMN_TARGETS"
],
"BRING \u00a72.1.2 p26 (scan p25)": [
"BRING.LAYOUT.COLUMNS.BALANCE_LENGTHS",
"BRING.LAYOUT.MEASURE.ADJUST_FOR_TYPE_SIZE",
"BRING.LAYOUT.MEASURE.AVOID_TOO_LONG",
"BRING.LAYOUT.MEASURE.AVOID_TOO_SHORT",
"BRING.LAYOUT.MEASURE.CHANGE_FOR_LISTS",
"BRING.LAYOUT.MEASURE.CODE_BLOCKS.WRAP_POLICY",
"BRING.LAYOUT.MEASURE.CONSISTENT_WITHIN_SECTION",
"BRING.LAYOUT.MEASURE.TARGET_RANGE_CHARS"
],
"BRING \u00a72.1.3 p27": [
"BRING.LAYOUT.JUSTIFICATION.RAGGED_RIGHT_IF_NEEDED"
],
"BRING \u00a72.1.4 p27": [
"BRING.TYPOGRAPHY.SPACING.SENTENCE_SPACE.SINGLE"
],
"BRING \u00a72.1.5 p30 (scan p29)": [
"BRING.TYPOGRAPHY.SPACING.INITIALS.THIN_SPACE"
],
"BRING \u00a72.1.6 p30 (scan p29)": [
"BRING.TYPOGRAPHY.NUMBER_STRINGS.SPACE_FOR_READABILITY",
"BRING.TYPOGRAPHY.TRACKING.CAPS_SMALLCAPS_AND_LONG_DIGITS"
],
"BRING \u00a72.1.7 p31 (scan p30)": [
"BRING.TYPOGRAPHY.TRACKING.LOWERCASE.AVOID"
],
"BRING \u00a72.1.8 p32-33": [
"BRING.TYPOGRAPHY.KERNING.CONSISTENT_OR_NONE"
],
"BRING \u00a72.1.9 p35 (scan p34)": [
"BRING.TYPOGRAPHY.LETTERFORMS.AVOID_DISTORTION"
],
"BRING \u00a72.2.1 p36 (scan p35)": [
"BRING.LAYOUT.LEADING.ADJUST_FOR_SIZE_CHANGES",
"BRING.LAYOUT.LEADING.ALIGN_BASELINE_GRID",
"BRING.LAYOUT.LEADING.AVOID_TOO_LOOSE",
"BRING.LAYOUT.LEADING.AVOID_TOO_TIGHT",
"BRING.LAYOUT.LEADING.CHOOSE_BASE",
"BRING.LAYOUT.LEADING.CONSISTENT_BODY"
],
"BRING \u00a72.2.2 p37 (scan p36)": [
"BRING.LAYOUT.LEADING.NEGATIVE.AVOID_CONTINUOUS_TEXT",
"BRING.LAYOUT.VERTICAL_RHYTHM.MEASURED_INTERVALS.MULTIPLES"
],
"BRING \u00a72.2.2 p38 (scan p37)": [
"BRING.LAYOUT.VERTICAL_RHYTHM.MEASURED_INTERVALS.MULTIPLES"
],
"BRING \u00a72.2.3 p39 (scan p38)": [
"BRING.LAYOUT.PAGE.DENSITY.DONT_SUFFOCATE"
],
"BRING \u00a72.3.1 p39 (scan p38)": [
"BRING.LAYOUT.PARAGRAPH.OPENING_FLUSH_LEFT"
],
"BRING \u00a72.3.2 p39 (scan p38)": [
"BRING.HEADINGS.PARAGRAPH_INDENT.AFTER_HEAD_NONE",
"BRING.LAYOUT.DEGRADED.HARD_WRAP_REFLOW",
"BRING.LAYOUT.PARAGRAPH.BLANK_LINES.SPARING",
"BRING.LAYOUT.PARAGRAPH.INDENT_AFTER_FIRST",
"BRING.LAYOUT.PARAGRAPH.INDENT_OR_SPACE_NOT_BOTH",
"BRING.LAYOUT.PARAGRAPH.INDENT_SIZE.CONSISTENT",
"BRING.LAYOUT.PARAGRAPH.NO_INDENT_AFTER_BLOCKS"
],
"BRING \u00a72.3.3 p40 (scan p39)": [
"BRING.HEADINGS.BLOCK_QUOTE.SPACING_AROUND",
"BRING.LAYOUT.BLOCK_QUOTES.AVOID_CROWDING",
"BRING.LAYOUT.BLOCK_QUOTES.BEFORE_AFTER_SPACING",
"BRING.LAYOUT.BLOCK_QUOTES.EXTRA_LEAD",
"BRING.LAYOUT.BLOCK_QUOTES.INDENT_OR_NARROW"
],
"BRING \u00a74.2 p65 (scan p64)": [
"BRING.HEADINGS.CAPITALIZATION.CONSISTENT",
"BRING.HEADINGS.SPACING.VERTICAL_RHYTHM",
"BRING.HEADINGS.STYLE.PALETTE_LIMIT",
"BRING.HEADINGS.SUBHEADS.STYLE_CONTRIBUTES_TO_WHOLE",
"BRING.HEADINGS.WEIGHT_SIZE.HIERARCHY_SCALE"
],
"BRING \u00a74.2.1 p65 (scan p64)": [
"BRING.HEADINGS.ALIGNMENT.CONSISTENT_LEVEL",
"BRING.HEADINGS.HIERARCHY.NO_SKIPPED_LEVELS",
"BRING.HEADINGS.MARGIN_HEADS.CLEAR_GUTTER",
"BRING.HEADINGS.RUN_IN.STANDALONE.CONSISTENT",
"BRING.HEADINGS.SUBHEADS.CROSSHEADS_SIDEHEADS.CHOOSE_DELIBERATELY",
"BRING.HEADINGS.SUBHEADS.LEVELS.AS_MANY_AS_NEEDED",
"BRING.HEADINGS.SUBHEADS.MARGIN_HEADS.RUNNING_SHOULDERHEADS",
"BRING.HEADINGS.SUBHEADS.RIGHT_SIDEHEADS.VISIBILITY"
],
"BRING \u00a74.2.2 p65 (scan p64)": [
"BRING.HEADINGS.CONTRAST.CLEAR_HIERARCHY",
"BRING.HEADINGS.SUBHEADS.MIXING.HIERARCHY_PLACEMENT",
"BRING.HEADINGS.SUBHEADS.MIXING_SYMM_ASYMM.AVOID_HAPHAZARD"
],
"BRING \u00a74.4 p70 (scan p69)": [
"BRING.TABLES.CAPTIONS.CLEAR",
"BRING.TABLES.TITLES.CONSISTENT_PLACEMENT"
],
"BRING \u00a74.4.1 p70 (scan p69)": [
"BRING.TABLES.COLUMN_ALIGNMENT.CONSISTENT",
"BRING.TABLES.DATA_TYPES.NOT_MIXED",
"BRING.TABLES.DEGRADED.REBUILD_COLUMNS",
"BRING.TABLES.EDIT_AS_TEXT.READABILITY",
"BRING.TABLES.FURNITURE.MINIMIZE",
"BRING.TABLES.GROUPING.WHITESPACE",
"BRING.TABLES.GUIDES.READING_DIRECTION",
"BRING.TABLES.HEADERS.ALIGN_WITH_COLUMNS",
"BRING.TABLES.HEADERS.CONCISE",
"BRING.TABLES.MULTI_LINE_HEADERS.AVOID",
"BRING.TABLES.NUMERIC_PRECISION.CONSISTENT",
"BRING.TABLES.ORDER.LOGICAL",
"BRING.TABLES.ROW_SPACING.READABLE",
"BRING.TABLES.SOURCES.NOTES.DISTINCT",
"BRING.TABLES.STUB_COLUMN.USE",
"BRING.TABLES.TEXT_ORIENTATION.HORIZONTAL",
"BRING.TABLES.TYPE_SIZE.READABLE",
"BRING.TABLES.UNITS.IN_HEADERS"
],
"CMOS18 \u00a71.10 p8 (scan p30)": [
"CMOS.HEADINGS.RUNNING_HEADS.DEFINITION",
"CMOS.HEADINGS.RUNNING_HEADS.DIVISION_MATCH",
"CMOS.HEADINGS.RUNNING_HEADS.LENGTH_SHORT",
"CMOS.HEADINGS.RUNNING_HEADS.NAVIGATION_SCOPE"
],
"CMOS18 \u00a71.56 p33 (scan p55)": [
"CMOS.HEADINGS.DIVISIONS.CHAPTERS.MULTIAUTHOR",
"CMOS.HEADINGS.MULTIAUTHOR.AUTHOR_ATTRIBUTION_PLACEMENT",
"CMOS.HEADINGS.MULTIAUTHOR.CHAPTER_NUMBERING"
],
"CMOS18 \u00a71.58 p33 (scan p55)": [
"CMOS.HEADINGS.DIVISIONS.LETTERS_DIARIES.HEADINGS",
"CMOS.HEADINGS.LETTERS_DIARIES.DATELINE_FORMAT",
"CMOS.HEADINGS.LETTERS_DIARIES.SIGNATURE_FORMAT"
],
"CMOS18 \u00a712.31 p746 (scan p768)": [
"CMOS.PUNCTUATION.BLOCK_QUOTES.NO_QUOTE_MARKS"
],
"CMOS18 \u00a712.59 p760 (scan p782)": [
"CMOS.PUNCTUATION.ELLIPSIS.FORMAT.CONSISTENT"
],
"CMOS18 \u00a713 p775": [
"CMOS.CITATIONS.MATCHING.BIBLIO_ENTRY_REQUIRED",
"CMOS.CITATIONS.QUOTATIONS.LOCATORS.PAGE_REQUIRED",
"CMOS.CITATIONS.SYSTEM.CONSISTENT_CHOICE",
"CMOS.CITATIONS.TITLES.CAPITALIZATION.CONSISTENT"
],
"CMOS18 \u00a713 p783": [
"CMOS.CITATIONS.NOTES_BIBLIO.FIRST_NOTE.FULL_REFERENCE"
],
"CMOS18 \u00a713 p819": [
"CMOS.CITATIONS.NOTES_BIBLIO.NAME_ORDER.NOTES_VS_BIBLIO"
],
"CMOS18 \u00a713 p833": [
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.PARENTHETICAL"
],
"CMOS18 \u00a713 p837": [
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.ORDER_AND_YEAR",
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.REQUIRED"
],
"CMOS18 \u00a713.112 p838 (scan p860)": [
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.ALPHABETICAL"
],
"CMOS18 \u00a713.113 p838 (scan p860)": [
"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.REPEATED_NAMES"
],
"CMOS18 \u00a713.117 p840 (scan p862)": [
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.LOCATORS"
],
"CMOS18 \u00a713.118 p841 (scan p863)": [
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.EXTRA_INFO"
],
"CMOS18 \u00a713.119 p841 (scan p863)": [
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.PLACEMENT"
],
"CMOS18 \u00a713.120 p842 (scan p864)": [
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.DIRECT_QUOTES"
],
"CMOS18 \u00a713.124 p844 (scan p866)": [
"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.MULTI_SOURCES"
],
"CMOS18 \u00a713.13 p781 (scan p803)": [
"CMOS.CITATIONS.RESEARCH.METADATA.CAPTURE_EARLY"
],
"CMOS18 \u00a713.14 p782 (scan p804)": [
"CMOS.CITATIONS.ONLINE.VERSION.CITE_RIGHT_VERSION"
],
"CMOS18 \u00a713.15 p782 (scan p804)": [
"CMOS.CITATIONS.ONLINE.ACCESS_DATE.WHEN_NEEDED"
],
"CMOS18 \u00a713.16 p782 (scan p804)": [
"CMOS.CITATIONS.ONLINE.REVISION_DATE.DISTINCT"
],
"CMOS18 \u00a713.25 p787 (scan p809)": [
"CMOS.CITATIONS.NOTES.CHAPTER_IN_EDITED_BOOK"
],
"CMOS18 \u00a713.26 p787 (scan p809)": [
"CMOS.CITATIONS.NOTES.JOURNAL.ARTICLE_ELEMENTS"
],
"CMOS18 \u00a713.27 p787 (scan p809)": [
"CMOS.CITATIONS.DEGRADED.NOTE_MARKERS.REPAIR",
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.SUPERSCRIPT_TEXT"
],
"CMOS18 \u00a713.28 p788 (scan p810)": [
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.SEQUENCE_CONTINUOUS"
],
"CMOS18 \u00a713.29 p788 (scan p810)": [
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.PLACEMENT_AFTER_PUNCT"
],
"CMOS18 \u00a713.30 p789 (scan p811)": [
"CMOS.CITATIONS.NOTES.NOTE_MARKERS.HEADINGS_END"
],
"CMOS18 \u00a713.31 p789 (scan p811)": [
"CMOS.CITATIONS.NOTES.MULTIPLE_CITATIONS.SINGLE_NOTE"
],
"CMOS18 \u00a713.32 p790 (scan p812)": [
"CMOS.CITATIONS.NOTES_BIBLIO.SUBSEQUENT_NOTES.SHORT_FORM"
],
"CMOS18 \u00a713.33 p790 (scan p812)": [
"CMOS.CITATIONS.NOTES.SHORT_FORM.BASIC_ELEMENTS"
],
"CMOS18 \u00a713.34 p790 (scan p812)": [
"CMOS.CITATIONS.NOTES.SHORT_FORM.CROSS_REFERENCE"
],
"CMOS18 \u00a713.37 p791 (scan p813)": [
"CMOS.CITATIONS.IBID.MINIMIZE_OR_AVOID"
],
"CMOS18 \u00a713.41 p794 (scan p816)": [
"CMOS.CITATIONS.NOTES.QUOTE_IN_NOTE.LOCATOR"
],
"CMOS18 \u00a713.42 p794 (scan p816)": [
"CMOS.CITATIONS.NOTES.SUBSTANTIVE.SEPARATE_FROM_SOURCE"
],
"CMOS18 \u00a713.43 p794 (scan p816)": [
"CMOS.CITATIONS.NOTES.LONG_NOTES.PARAGRAPHING"
],
"CMOS18 \u00a713.44 p794 (scan p816)": [
"CMOS.CITATIONS.NOTES.FOOTNOTES.PAGE_BREAKS"
],
"CMOS18 \u00a713.46 p796 (scan p818)": [
"CMOS.CITATIONS.NOTES.FOOTNOTES_VS_ENDNOTES.CHOOSE"
],
"CMOS18 \u00a713.49 p797 (scan p819)": [
"CMOS.CITATIONS.NOTES.ENDNOTES.PLACEMENT"
],
"CMOS18 \u00a713.50 p797 (scan p819)": [
"CMOS.CITATIONS.NOTES.ENDNOTES.RUNNING_HEADS"
],
"CMOS18 \u00a713.51 p800 (scan p822)": [
"CMOS.CITATIONS.NOTES.ENDNOTES.AVOID_IBID"
],
"CMOS18 \u00a713.53 p801 (scan p823)": [
"CMOS.CITATIONS.NOTES.AUTHOR_DATE_PLUS_NOTES"
],
"CMOS18 \u00a713.55 p801 (scan p823)": [
"CMOS.CITATIONS.NOTES.UNNUMBERED.NOT_FOR_SOURCES"
],
"CMOS18 \u00a713.58 p805 (scan p827)": [
"CMOS.CITATIONS.NOTES.SOURCE_NOTES.REPRINTS"
],
"CMOS18 \u00a713.6 p778 (scan p800)": [
"CMOS.CITATIONS.DEGRADED.URL_LINEBREAKS.NORMALIZE",
"CMOS.CITATIONS.ONLINE.URLS.STABLE"
],
"CMOS18 \u00a713.60 p807 (scan p829)": [
"CMOS.CITATIONS.NOTES.AVOID_OVERLONG"
],
"CMOS18 \u00a713.65 p809 (scan p831)": [
"CMOS.CITATIONS.NOTES_BIBLIO.BIBLIOGRAPHY.INCLUDE_WHEN_USED"
],
"CMOS18 \u00a713.69 p816 (scan p838)": [
"CMOS.CITATIONS.NOTES_BIBLIO.BIBLIOGRAPHY.SORT_BY_AUTHOR"
],
"CMOS18 \u00a713.7 p778 (scan p800)": [
"CMOS.CITATIONS.DOI.PREFERRED_OVER_URL"
],
"CMOS18 \u00a713.70 p816 (scan p838)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.SAME_AUTHOR.ORDER"
],
"CMOS18 \u00a713.73 p818 (scan p840)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.REPEATED_NAMES.THREE_EM_DASH"
],
"CMOS18 \u00a713.74 p819 (scan p841)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.CONSISTENT_FORM"
],
"CMOS18 \u00a713.75 p819 (scan p841)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.PUBLISHED_FORM"
],
"CMOS18 \u00a713.76 p820 (scan p842)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.INITIALS_PREFERRED"
],
"CMOS18 \u00a713.78 p821 (scan p843)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.MULTI_AUTHORS.ORDER"
],
"CMOS18 \u00a713.79 p821 (scan p843)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.SAME_SURNAME.DISAMBIGUATE"
],
"CMOS18 \u00a713.8 p779 (scan p801)": [
"CMOS.CITATIONS.ONLINE.PERMALINKS.PREFERRED"
],
"CMOS18 \u00a713.81 p822 (scan p844)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.NO_AUTHOR.TITLE_LEAD"
],
"CMOS18 \u00a713.82 p823 (scan p845)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.PSEUDONYMS.CONSISTENT"
],
"CMOS18 \u00a713.84 p824 (scan p846)": [
"CMOS.CITATIONS.BIBLIOGRAPHY.ALT_NAMES.CROSSREF"
],
"CMOS18 \u00a714 p935": [
"CMOS.CITATIONS.LEGAL_PUBLIC_DOCS.USE_JURISDICTIONAL_FORMAT"
],
"CMOS18 \u00a76.101 p422 (scan p444)": [
"CMOS.PUNCTUATION.PARENS.USE",
"CMOS.PUNCTUATION.PARENS_BRACKETS.BALANCE"
],
"CMOS18 \u00a76.102 p422 (scan p444)": [
"CMOS.PUNCTUATION.PARENS.GLOSSES_TRANSLATIONS"
],
"CMOS18 \u00a76.103 p422 (scan p444)": [
"CMOS.PUNCTUATION.PARENS.NESTING",
"CMOS.PUNCTUATION.PARENS_BRACKETS.BALANCE"
],
"CMOS18 \u00a76.106 p424 (scan p446)": [
"CMOS.PUNCTUATION.BRACKETS.TRANSLATED_TEXT"
],
"CMOS18 \u00a76.107 p424 (scan p446)": [
"CMOS.PUNCTUATION.BRACKETS.NESTED_PARENS"
],
"CMOS18 \u00a76.113 p426 (scan p448)": [
"CMOS.PUNCTUATION.SLASHES.ALTERNATIVES"
],
"CMOS18 \u00a76.114 p426 (scan p448)": [
"CMOS.PUNCTUATION.SLASHES.TWO_YEAR_SPANS"
],
"CMOS18 \u00a76.12 p383 (scan p405)": [
"CMOS.PUNCTUATION.PERIODS.USE"
],
"CMOS18 \u00a76.122 p428 (scan p450)": [
"CMOS.PUNCTUATION.QUOTATION_MARKS.DOUBLE_PRIMARY_US",
"CMOS.PUNCTUATION.QUOTATION_MARKS.PUNCTUATION_PLACEMENT_US"
],
"CMOS18 \u00a76.123 p428 (scan p450)": [
"CMOS.PUNCTUATION.QUOTES.SMART_QUOTES"
],
"CMOS18 \u00a76.13 p383 (scan p405)": [
"CMOS.PUNCTUATION.PERIODS.WITH_PARENS"
],
"CMOS18 \u00a76.131 p432 (scan p454)": [
"CMOS.PUNCTUATION.MULTIPLE_MARKS.AVOID_STACKING"
],
"CMOS18 \u00a76.17 p385 (scan p407)": [
"CMOS.PUNCTUATION.COMMAS.NONRESTRICTIVE.SET_OFF"
],
"CMOS18 \u00a76.19 p385 (scan p407)": [
"CMOS.PUNCTUATION.COMMAS.SERIAL_COMMA.DEFAULT"
],
"CMOS18 \u00a76.24 p388 (scan p410)": [
"CMOS.PUNCTUATION.COMMAS.COMPOUND_PREDICATES"
],
"CMOS18 \u00a76.26 p390 (scan p412)": [
"CMOS.PUNCTUATION.COMMAS.INTRODUCTORY_ELEMENTS.CLARITY"
],
"CMOS18 \u00a76.27 p390 (scan p412)": [
"CMOS.PUNCTUATION.COMMAS.DEPENDENT_CLAUSE_AFTER"
],
"CMOS18 \u00a76.29 p392 (scan p414)": [
"CMOS.PUNCTUATION.COMMAS.RESTRICTIVE.NO_SET_OFF"
],
"CMOS18 \u00a76.30 p392 (scan p414)": [
"CMOS.PUNCTUATION.COMMAS.APPOSITIVES"
],
"CMOS18 \u00a76.32 p394 (scan p416)": [
"CMOS.PUNCTUATION.COMMAS.DESCRIPTIVE_PHRASES"
],
"CMOS18 \u00a76.33 p394 (scan p416)": [
"CMOS.PUNCTUATION.COMMAS.PARTICIPIAL_PHRASES"
],
"CMOS18 \u00a76.36 p396 (scan p418)": [
"CMOS.PUNCTUATION.COMMAS.INTRODUCTORY_PHRASES"
],
"CMOS18 \u00a76.40 p398 (scan p420)": [
"CMOS.PUNCTUATION.COMMAS.REPEATED_ADJECTIVES"
],
"CMOS18 \u00a76.41 p398 (scan p420)": [
"CMOS.PUNCTUATION.COMMAS.DATES"
],
"CMOS18 \u00a76.42 p398 (scan p420)": [
"CMOS.PUNCTUATION.COMMAS.ADDRESSES"
],
"CMOS18 \u00a76.44 p400 (scan p422)": [
"CMOS.PUNCTUATION.COMMAS.QUOTED_TITLES"
],
"CMOS18 \u00a76.45 p400 (scan p422)": [
"CMOS.PUNCTUATION.COMMAS.QUESTIONS"
],
"CMOS18 \u00a76.62 p408 (scan p430)": [
"CMOS.PUNCTUATION.SEMICOLONS.CONJUNCTIVE_PHRASES"
],
"CMOS18 \u00a76.63 p408 (scan p430)": [
"CMOS.PUNCTUATION.SEMICOLONS.BEFORE_CONJUNCTION"
],
"CMOS18 \u00a76.64 p408 (scan p430)": [
"CMOS.PUNCTUATION.SEMICOLONS.COMPLEX_SERIES.SEPARATE"
],
"CMOS18 \u00a76.66 p410 (scan p432)": [
"CMOS.PUNCTUATION.COLONS.SPACE_AFTER",
"CMOS.PUNCTUATION.DEGRADED.EXTRA_SPACES.AFTER_PUNCT"
],
"CMOS18 \u00a76.67 p410 (scan p432)": [
"CMOS.PUNCTUATION.COLONS.CAPITALIZATION"
],
"CMOS18 \u00a76.68 p410 (scan p432)": [
"CMOS.PUNCTUATION.COLONS.AS_FOLLOWS"
],
"CMOS18 \u00a76.69 p410 (scan p432)": [
"CMOS.PUNCTUATION.COLONS.INTRO_QUOTE_QUESTION"
],
"CMOS18 \u00a76.72 p412 (scan p434)": [
"CMOS.PUNCTUATION.QUESTION_MARK.USE"
],
"CMOS18 \u00a76.73 p412 (scan p434)": [
"CMOS.PUNCTUATION.QUESTION_MARK.DIRECT_VS_INDIRECT"
],
"CMOS18 \u00a76.74 p413 (scan p435)": [
"CMOS.PUNCTUATION.QUESTION_MARK.WITH_PUNCT"
],
"CMOS18 \u00a76.75 p413 (scan p435)": [
"CMOS.PUNCTUATION.EXCLAMATION.USE_SPARINGLY"
],
"CMOS18 \u00a76.76 p413 (scan p435)": [
"CMOS.PUNCTUATION.EXCLAMATION.VS_QUESTION"
],
"CMOS18 \u00a76.83 p415 (scan p437)": [
"CMOS.NUMBERS.RANGES.EN_DASH.USE",
"CMOS.PUNCTUATION.DASHES.EN_DASH.RANGES"
],
"CMOS18 \u00a76.89 p418 (scan p440)": [
"CMOS.PUNCTUATION.DASHES.EM_DASH.USE_WITHOUT_SPACES_US"
],
"CMOS18 \u00a76.91 p418 (scan p440)": [
"CMOS.PUNCTUATION.DASHES.EM_DASH.USE_WITHOUT_SPACES_US"
],
"CMOS18 \u00a76.96 p420 (scan p442)": [
"CMOS.PUNCTUATION.DASHES.EM_LINE_BREAKS"
],
"CMOS18 \u00a76.97 p420 (scan p442)": [
"CMOS.PUNCTUATION.DASHES.EM_INSTEAD_OF_QUOTES"
],
"CMOS18 \u00a77.87 p474 (scan p496)": [
"CMOS.PUNCTUATION.HYPHENATION.GENERAL_CHOICE"
],
"CMOS18 \u00a77.88 p475 (scan p497)": [
"CMOS.PUNCTUATION.HYPHENATION.COMPOUND_DEFINITION"
],
"CMOS18 \u00a77.89 p475 (scan p497)": [
"CMOS.PUNCTUATION.HYPHENATION.TREND_CLOSED"
],
"CMOS18 \u00a77.90 p475 (scan p497)": [
"CMOS.PUNCTUATION.HYPHENATION.READABILITY"
],
"CMOS18 \u00a77.91 p476 (scan p498)": [
"CMOS.PUNCTUATION.HYPHENS.COMPOUND_MODIFIERS.BEFORE_NOUN"
],
"CMOS18 \u00a77.93 p476 (scan p498)": [
"CMOS.PUNCTUATION.HYPHENS.ADVERB_LY.NO_HYPHEN"
],
"CMOS18 \u00a77.95 p477 (scan p499)": [
"CMOS.PUNCTUATION.HYPHENATION.SUSPENDED"
],
"CMOS18 \u00a79.10 p596 (scan p618)": [
"CMOS.NUMBERS.SCIENTIFIC.POWERS_OF_TEN"
],
"CMOS18 \u00a79.11 p596 (scan p618)": [
"CMOS.NUMBERS.SI_PREFIXES.NO_HYPHEN"
],
"CMOS18 \u00a79.12 p596 (scan p618)": [
"CMOS.NUMBERS.BASES.NON_DECIMAL.NO_GROUPING"
],
"CMOS18 \u00a79.14 p597 (scan p619)": [
"CMOS.NUMBERS.NUMERALS.MEASUREMENTS.UNITS"
],
"CMOS18 \u00a79.15 p597 (scan p619)": [
"CMOS.NUMBERS.FRACTIONS.SIMPLE.SPELL_OUT"
],
"CMOS18 \u00a79.16 p598 (scan p620)": [
"CMOS.NUMBERS.FRACTIONS.MIXED.WHOLE_PLUS_FRACTION"
],
"CMOS18 \u00a79.17 p598 (scan p620)": [
"CMOS.NUMBERS.FRACTIONS.MATH.NUMERALS"
],
"CMOS18 \u00a79.18 p598 (scan p620)": [
"CMOS.NUMBERS.NUMERALS.MEASUREMENTS.UNITS"
],
"CMOS18 \u00a79.19 p599 (scan p621)": [
"CMOS.NUMBERS.DEGRADED.HARD_WRAP_UNITS",
"CMOS.NUMBERS.UNITS.REPEATED.OMIT_REPEAT"
],
"CMOS18 \u00a79.2 p590 (scan p612)": [
"CMOS.NUMBERS.CONSISTENCY.MIXED_FORMS.AVOID",
"CMOS.NUMBERS.RULE_SELECTION.GENERAL_OR_ALTERNATIVE",
"CMOS.NUMBERS.SPELLING.ONE_TO_ONE_HUNDRED.DEFAULT"
],
"CMOS18 \u00a79.20 p599 (scan p621)": [
"CMOS.NUMBERS.PERCENTAGES.NUMERALS"
],
"CMOS18 \u00a79.21 p600 (scan p622)": [
"CMOS.NUMBERS.DECIMALS.LEADING_ZERO",
"CMOS.NUMBERS.DECIMALS.NUMERALS"
],
"CMOS18 \u00a79.22 p600 (scan p622)": [
"CMOS.NUMBERS.CURRENCY.FORMAT.SYMBOL_PLACEMENT",
"CMOS.NUMBERS.CURRENCY.WORDS_VS_SYMBOLS"
],
"CMOS18 \u00a79.23 p601 (scan p623)": [
"CMOS.NUMBERS.CURRENCY.NON_US.DISAMBIGUATE"
],
"CMOS18 \u00a79.25 p602 (scan p624)": [
"CMOS.NUMBERS.CURRENCY.ISO_CODES"
],
"CMOS18 \u00a79.26 p602 (scan p624)": [
"CMOS.NUMBERS.CURRENCY.LARGE_AMOUNTS"
],
"CMOS18 \u00a79.27 p602 (scan p624)": [
"CMOS.NUMBERS.CURRENCY.HISTORICAL.YEAR_CONTEXT"
],
"CMOS18 \u00a79.28 p603 (scan p625)": [
"CMOS.NUMBERS.REFERENCES.PAGE_CHAPTER_FIGURE"
],
"CMOS18 \u00a79.29 p603 (scan p625)": [
"CMOS.NUMBERS.PERIODICALS.VOLUME_ISSUE"
],
"CMOS18 \u00a79.3 p590 (scan p612)": [
"CMOS.NUMBERS.RULE_SELECTION.ALTERNATIVE_ZERO_TO_NINE"
],
"CMOS18 \u00a79.31 p604 (scan p626)": [
"CMOS.NUMBERS.DATES.CONSISTENT_FORMAT",
"CMOS.NUMBERS.DATES.YEAR_NUMERALS"
],
"CMOS18 \u00a79.32 p604 (scan p626)": [
"CMOS.NUMBERS.DATES.ABBREVIATED_YEAR"
],
"CMOS18 \u00a79.33 p604 (scan p626)": [
"CMOS.NUMBERS.DATES.MONTH_DAY_STYLE"
],
"CMOS18 \u00a79.34 p605 (scan p627)": [
"CMOS.NUMBERS.CENTURIES.SPELLED_OUT"
],
"CMOS18 \u00a79.35 p605 (scan p627)": [
"CMOS.NUMBERS.DECADES.CONSISTENT_FORM"
],
"CMOS18 \u00a79.36 p606 (scan p628)": [
"CMOS.NUMBERS.ERAS.BCE_CE"
],
"CMOS18 \u00a79.37 p607 (scan p629)": [
"CMOS.NUMBERS.DATES.ALL_NUMERAL"
],
"CMOS18 \u00a79.38 p607 (scan p629)": [
"CMOS.NUMBERS.DATES.ISO_8601"
],
"CMOS18 \u00a79.39 p607 (scan p629)": [
"CMOS.NUMBERS.TIME.GENERAL_NUMERALS"
],
"CMOS18 \u00a79.4 p591 (scan p613)": [
"CMOS.NUMBERS.ROUND_NUMBERS.SPELL_OUT"
],
"CMOS18 \u00a79.40 p608 (scan p630)": [
"CMOS.NUMBERS.TIME.NOON_MIDNIGHT"
],
"CMOS18 \u00a79.41 p608 (scan p630)": [
"CMOS.NUMBERS.TIME.TWENTY_FOUR_HOUR"
],
"CMOS18 \u00a79.42 p609 (scan p631)": [
"CMOS.NUMBERS.TIME.ISO_STYLE"
],
"CMOS18 \u00a79.43 p609 (scan p631)": [
"CMOS.NUMBERS.NAMES.MONARCHS_POPES"
],
"CMOS18 \u00a79.46 p610 (scan p632)": [
"CMOS.NUMBERS.VEHICLES.VESSELS_NUMBERS"
],
"CMOS18 \u00a79.5 p591 (scan p613)": [
"CMOS.NUMBERS.SENTENCE_START.AVOID_NUMERAL"
],
"CMOS18 \u00a79.52 p611 (scan p633)": [
"CMOS.NUMBERS.PLACES.HIGHWAYS"
],
"CMOS18 \u00a79.53 p611 (scan p633)": [
"CMOS.NUMBERS.PLACES.STREETS"
],
"CMOS18 \u00a79.54 p611 (scan p633)": [
"CMOS.NUMBERS.PLACES.BUILDINGS_APTS"
],
"CMOS18 \u00a79.55 p612 (scan p634)": [
"CMOS.NUMBERS.PLURALS.DECADE.NO_APOSTROPHE",
"CMOS.NUMBERS.PLURALS.SPELLED_OUT"
],
"CMOS18 \u00a79.56 p612 (scan p634)": [
"CMOS.NUMBERS.GROUPING.THOUSANDS_SEPARATOR"
],
"CMOS18 \u00a79.57 p613 (scan p635)": [
"CMOS.NUMBERS.DECIMAL_MARKER.LOCALE",
"CMOS.NUMBERS.DEGRADED.NUMERAL_NORMALIZATION"
],
"CMOS18 \u00a79.58 p613 (scan p635)": [
"CMOS.NUMBERS.DIGIT_GROUPING.SI_SPACE"
],
"CMOS18 \u00a79.59 p614 (scan p636)": [
"CMOS.NUMBERS.TELEPHONE.FORMAT"
],
"CMOS18 \u00a79.6 p592 (scan p614)": [
"CMOS.NUMBERS.ORDINALS.SUFFIX.CORRECT"
],
"CMOS18 \u00a79.60 p615 (scan p637)": [
"CMOS.NUMBERS.RATIOS.FORMAT"
],
"CMOS18 \u00a79.61 p615 (scan p637)": [
"CMOS.NUMBERS.LISTS.OUTLINE.NUMERAL_STYLE"
],
"CMOS18 \u00a79.62 p615 (scan p637)": [
"CMOS.NUMBERS.RANGES.EN_DASH.USE"
],
"CMOS18 \u00a79.63 p616 (scan p638)": [
"CMOS.NUMBERS.INCLUSIVE_RANGES.PAGE_NUMBERS.SHORTEN"
],
"CMOS18 \u00a79.65 p617 (scan p639)": [
"CMOS.NUMBERS.INCLUSIVE.COMMAS"
],
"CMOS18 \u00a79.66 p617 (scan p639)": [
"CMOS.NUMBERS.INCLUSIVE.YEARS"
],
"CMOS18 \u00a79.67 p617 (scan p639)": [
"CMOS.NUMBERS.ROMAN_NUMERALS.USE"
],
"CMOS18 \u00a79.7 p592 (scan p614)": [
"CMOS.NUMBERS.DENSE_CONTEXT.USE_NUMERALS"
],
"CMOS18 \u00a79.8 p593 (scan p615)": [
"CMOS.NUMBERS.CONTEXTS.ALWAYS_NUMERALS"
],
"CMOS18 \u00a79.9 p595 (scan p617)": [
"CMOS.NUMBERS.LARGE_VALUES.MILLIONS_BILLIONS"
],
"HOUSE \u00a7A11Y.BASICS p2": [
"HOUSE.A11Y.DOCUMENT_LANGUAGE.DECLARE",
"HOUSE.A11Y.HEADINGS.NO_SKIPS",
"HOUSE.A11Y.IMAGES.ALT_REQUIRED",
"HOUSE.A11Y.LINK_TEXT.DESCRIPTIVE",
"HOUSE.LINKS.TEXT.DESCRIPTIVE"
],
"HOUSE \u00a7QA.CODE_OVERFLOW p2": [
"HOUSE.CODE.BLOCKS.LANGUAGE_TAGS.PREFERRED",
"HOUSE.CODE.BLOCKS.NO_CLIPPING",
"HOUSE.CODE.BLOCKS.WRAP_POLICY",
"HOUSE.CODE.INLINE.MONO_BACKTICKS"
],
"HOUSE \u00a7QA.KEEPS p1": [
"HOUSE.HEADINGS.KEEPS.AVOID_STRANDED",
"HOUSE.LAYOUT.PAGINATION.KEEP_WITH_NEXT.HEADINGS"
],
"HOUSE \u00a7QA.LINK_WRAP p2": [
"HOUSE.LINKS.DISALLOW.FILE_URIS",
"HOUSE.LINKS.PUNCTUATION.NO_TRAILING_PUNCT",
"HOUSE.LINKS.URLS.PREFER_HTTPS",
"HOUSE.LINKS.WRAP.SAFE_BREAKS"
],
"HOUSE \u00a7QA.OVERFLOW p1": [
"HOUSE.LAYOUT.OVERFLOW.OVERFULL_LINES.REPORT"
],
"HOUSE \u00a7QA.TABLE_OVERFLOW p2": [
"HOUSE.TABLES.ALIGNMENT.DECIMALS",
"HOUSE.TABLES.HEADERS.REPEAT_ON_PAGEBREAK",
"HOUSE.TABLES.OVERFLOW.NO_CLIPPING"
]
}

142
spec/manifest.yaml Normal file
View file

@ -0,0 +1,142 @@
version: "0.1.0"
registry_id: "pubstyle"
description: >
Machine-readable style+typesetting rules for a Markdown→HTML→PDF pipeline,
backed by primary references (Chicago / Bringhurst) and optional house rules.
Rules are paraphrases only; sources are referenced by pointer strings.
id_naming:
prefixes:
CMOS: "Editorial/style usage rules derived primarily from Chicago."
BRING: "Typographic/layout rules derived primarily from Bringhurst."
HOUSE: "Project-specific rules not directly sourced to Chicago/Bringhurst."
pattern: "PREFIX.DOMAIN.TOPIC[.SUBTOPIC[.DETAIL...]]"
delimiter: "."
casing: "UPPER_SNAKE for segments"
stability:
rule_ids_are_immutable: true
rename_policy: "Deprecate old id; introduce new id; keep mapping in report diffs."
examples:
- "CMOS.PUNCTUATION.DASHES.EM_DASH"
- "BRING.LAYOUT.WIDOWS_ORPHANS.AVOID"
- "HOUSE.CITATIONS.DOI.PREFER_HTTPS"
source_pointer_scheme:
goal: "Provide auditable traceability without reproducing sources."
pointer_format_primary: "CMOS18 §<section> p<book_page>"
pointer_format_secondary: "BRING §<section> p<book_page>"
pointer_format_house: "HOUSE §<section> p<doc_page>"
optional_scan_hint: "(scan p<pdf_page_index>)"
allowed_page_numbering:
- arabic
- roman
notes:
- "Pointers must be sufficient for a reader with the book to locate the guidance."
- "Never store verbatim passages; paraphrase only."
- "If a rule depends on exact wording, rule_text must say: Exact wording required—refer to pointer."
category_taxonomy:
- editorial
- typography
- layout
- headings
- citations
- numbers
- punctuation
- abbreviations
- links
- tables
- figures
- code
- frontmatter
- backmatter
- accessibility
- i18n
profiles:
- web_pdf
- print_pdf
- dense_tech
- memo
- slide_deck
planned_rule_counts:
target_total_range: [800, 1500]
target_by_category:
editorial: 120
typography: 170
layout: 140
headings: 70
citations: 140
numbers: 90
punctuation: 120
abbreviations: 60
links: 50
tables: 60
figures: 50
code: 70
frontmatter: 40
backmatter: 40
accessibility: 90
i18n: 60
coverage_contract:
must_rules:
enforceability_requirement: >
Every MUST rule must be enforceable by at least one of: lint, typeset, postrender;
otherwise it must be explicitly labeled as a manual checklist item and emitted in
a checklist output artifact.
manual_checklist_tag: "manual_checklist=true"
checklist_artifact: "manual-checklist.md (and JSON mirror)"
should_rules:
policy: "Should rules should be enforceable when practical; otherwise allowed as manual with explicit rationale."
warn_rules:
policy: "Warnings may be non-blocking and advisory; still require source pointers."
enforcement_definitions:
lint: "Static analysis over normalized Markdown/HTML AST. Deterministic."
typeset: "CSS/tokens shaping decisions prior to rendering (pagination, keeps, hyphenation parameters)."
postrender: "PDF/HTML layout inspection (widows/orphans, overflow, keep failures, numbering mismatches)."
manual: "Human review; system must still produce checklist items and traceability pointers."
ci_guardrails:
coverage_floor:
must_implemented_min_percent: 95
overall_implemented_min_percent: 80
regression_rule: "CI fails if implemented coverage decreases from main branch."
degraded_mode_contract:
purpose: "Handle badly-structured inputs safely without crashing; still provide useful output."
triggers:
- "Markdown parse errors / invalid UTF-8"
- "Missing heading hierarchy (no H1/H2 etc.)"
- "Garbage extraction (e.g., line breaks every word, excessive hard wraps)"
- "Mixed language with no lang metadata"
behavior:
normalize:
attempt_repairs:
- "Normalize whitespace and line endings"
- "Detect and unwrap hard-wrapped paragraphs heuristically"
- "Infer heading levels from patterns (e.g., 1., 1.1, ALL CAPS lines) with low confidence"
if_unrecoverable:
- "Fall back to minimal AST: paragraphs + code blocks + raw spans"
- "Mark document structure confidence = low"
enforcement_in_degraded_mode:
lint:
run_subset:
- "safety"
- "sanity"
- "catastrophic typography (double spaces, broken links)"
downgrade_some_must_to_warn: true
typeset:
use_fallback_tokens: true
disable_aggressive_hyphenation: true
postrender:
run_core_gates_only:
- "overfull_lines"
- "table_overflow_incidents"
- "code_overflow_incidents"
reporting:
always_emit:
- "layout-report.json"
- "coverage-report.json"
- "degraded-mode-report.json (what was inferred and why)"

View file

@ -0,0 +1,84 @@
profile_id: "dense_tech"
description: "Technical papers and specs: denser copy, more code/table tolerance, strict numbering and citations."
page:
size: "A4"
orientation: "portrait"
two_sided: false
margins:
top: "18mm"
bottom: "18mm"
inner: "18mm"
outer: "18mm"
fonts:
body:
family: ["Noto Serif", "STIX Two Text", "serif"]
size: "10pt"
line_height: 1.35
heading:
family: ["Noto Sans", "Source Sans 3", "sans-serif"]
mono:
family: ["Noto Sans Mono", "Source Code Pro", "monospace"]
size: "9pt"
line_height: 1.25
measure_targets:
columns: 1
body_chars_per_line:
min: 65
ideal: 75
max: 90
hyphenation:
enabled: true
strategy: "balanced"
min_left: 2
min_right: 3
max_consecutive_hyphenated_lines: 3
avoid_proper_names_when_possible: true
headings:
keep_with_next_lines: 2
avoid_stranded_headings: true
numbering:
enabled: true
style: "decimal"
require_monotonic_increase: true
widows_orphans:
widow_lines: 2
orphan_lines: 2
balance_facing_pages: false
code:
block:
font_size: "8.8pt"
line_height: 1.20
wrap: true
overflow_policy: "wrap_then_shrink_minor"
shrink_limit: 0.90
tables:
cell_padding: "2pt 4pt"
header_repeat: true
overflow_policy: "shrink_then_wrap"
shrink_limit: 0.85
severity_overrides:
- selector: { category: "citations" }
severity: "must"
- selector: { category: "headings", tag: "numbering" }
severity: "must"
- selector: { category: "layout", tag: "widows_orphans" }
severity: "should"
locale_defaults:
primary_language: "en"
fallback_languages: ["fr"]
quotation_style: "us"
date_format: "YYYY-MM-DD"
number_format:
decimal_separator: "."
thousands_separator: ","

76
spec/profiles/memo.yaml Normal file
View file

@ -0,0 +1,76 @@
profile_id: "memo"
description: "Short internal documents: lenient pagination, strong clarity, minimal typographic complexity."
page:
size: "Letter"
orientation: "portrait"
two_sided: false
margins:
top: "1in"
bottom: "1in"
inner: "1in"
outer: "1in"
fonts:
body:
family: ["Noto Sans", "Source Sans 3", "Arial", "sans-serif"]
size: "11pt"
line_height: 1.40
heading:
family: ["Noto Sans", "Source Sans 3", "Arial", "sans-serif"]
mono:
family: ["Noto Sans Mono", "Consolas", "monospace"]
size: "10pt"
line_height: 1.30
measure_targets:
columns: 1
body_chars_per_line:
min: 55
ideal: 70
max: 85
hyphenation:
enabled: false
strategy: "off_for_memos"
headings:
keep_with_next_lines: 2
avoid_stranded_headings: true
numbering:
enabled: false
widows_orphans:
widow_lines: 1
orphan_lines: 1
balance_facing_pages: false
code:
block:
font_size: "9.5pt"
line_height: 1.25
wrap: true
overflow_policy: "wrap"
shrink_limit: 1.0
tables:
cell_padding: "3pt 6pt"
header_repeat: false
overflow_policy: "wrap"
shrink_limit: 1.0
severity_overrides:
- selector: { category: "layout", tag: "widows_orphans" }
severity: "warn"
- selector: { category: "accessibility" }
severity: "must"
locale_defaults:
primary_language: "en"
fallback_languages: ["fr"]
quotation_style: "us"
date_format: "YYYY-MM-DD"
number_format:
decimal_separator: "."
thousands_separator: ","

View file

@ -0,0 +1,88 @@
profile_id: "print_pdf"
description: "Print-oriented PDF with stricter pagination, book-like rhythm, and stronger keep constraints."
page:
size: "6in×9in"
orientation: "portrait"
two_sided: true
margins:
top: "18mm"
bottom: "20mm"
inner: "22mm"
outer: "18mm"
fonts:
body:
family: ["STIX Two Text", "Noto Serif", "Georgia", "serif"]
size: "10.5pt"
line_height: 1.50
heading:
family: ["STIX Two Text", "Noto Serif", "serif"]
mono:
family: ["Noto Sans Mono", "Source Code Pro", "Consolas", "monospace"]
size: "9.5pt"
line_height: 1.30
measure_targets:
columns: 1
body_chars_per_line:
min: 55
ideal: 66
max: 72
hyphenation:
enabled: true
strategy: "print_quality"
min_left: 2
min_right: 3
max_consecutive_hyphenated_lines: 2
avoid_proper_names_when_possible: true
paragraphs:
first_paragraph_indent: "0"
indent: "1em"
headings:
keep_with_next_lines: 3
avoid_stranded_headings: true
numbering:
enabled: true
style: "decimal"
require_monotonic_increase: true
widows_orphans:
widow_lines: 2
orphan_lines: 2
balance_facing_pages: true
code:
block:
font_size: "9pt"
line_height: 1.25
wrap: false
overflow_policy: "shrink_then_scroll_indicator"
shrink_limit: 0.90
tables:
cell_padding: "2.5pt 5pt"
header_repeat: true
overflow_policy: "shrink_then_rotate_if_allowed"
shrink_limit: 0.88
severity_overrides:
- selector: { category: "layout", tag: "widows_orphans" }
severity: "must"
- selector: { category: "layout", tag: "keep_constraints" }
severity: "must"
- selector: { category: "typography", tag: "spacing_consistency" }
severity: "must"
locale_defaults:
primary_language: "en"
fallback_languages: ["fr"]
quotation_style: "us"
date_format: "Month D, YYYY"
number_format:
decimal_separator: "."
thousands_separator: ","

View file

@ -0,0 +1,76 @@
profile_id: "slide_deck"
description: "Paged slides (16:9). Emphasis on hierarchy, short lines, and avoiding overflows."
page:
size: "13.333in×7.5in"
orientation: "landscape"
two_sided: false
margins:
top: "0.5in"
bottom: "0.5in"
inner: "0.6in"
outer: "0.6in"
fonts:
body:
family: ["Noto Sans", "Source Sans 3", "Arial", "sans-serif"]
size: "24pt"
line_height: 1.15
heading:
family: ["Noto Sans", "Source Sans 3", "Arial", "sans-serif"]
mono:
family: ["Noto Sans Mono", "Consolas", "monospace"]
size: "20pt"
line_height: 1.10
measure_targets:
columns: 1
body_chars_per_line:
min: 25
ideal: 40
max: 55
hyphenation:
enabled: false
strategy: "off_for_slides"
headings:
keep_with_next_lines: 1
avoid_stranded_headings: true
numbering:
enabled: false
widows_orphans:
widow_lines: 1
orphan_lines: 1
balance_facing_pages: false
code:
block:
font_size: "18pt"
line_height: 1.10
wrap: true
overflow_policy: "wrap_then_shrink_minor"
shrink_limit: 0.92
tables:
cell_padding: "6pt 10pt"
header_repeat: false
overflow_policy: "shrink_then_wrap"
shrink_limit: 0.88
severity_overrides:
- selector: { category: "layout", tag: "overflow" }
severity: "must"
- selector: { category: "accessibility" }
severity: "must"
locale_defaults:
primary_language: "en"
fallback_languages: ["fr"]
quotation_style: "us"
date_format: "YYYY-MM-DD"
number_format:
decimal_separator: "."
thousands_separator: ","

View file

@ -0,0 +1,96 @@
profile_id: "web_pdf"
description: "Screen-first PDF for sharing and reading; conservative pagination and strong accessibility defaults."
page:
size: "Letter"
orientation: "portrait"
two_sided: false
margins:
top: "22mm"
bottom: "22mm"
inner: "20mm"
outer: "20mm"
fonts:
body:
family: ["Noto Serif", "STIX Two Text", "Times New Roman", "serif"]
size: "11pt"
line_height: 1.45
heading:
family: ["Noto Sans", "Source Sans 3", "Arial", "sans-serif"]
mono:
family: ["Noto Sans Mono", "Source Code Pro", "Consolas", "monospace"]
size: "10pt"
line_height: 1.35
measure_targets:
columns: 1
body_chars_per_line:
min: 55
ideal: 66
max: 75
footnote_chars_per_line:
min: 50
ideal: 60
max: 70
hyphenation:
enabled: true
strategy: "balanced"
language_driven: true
min_left: 2
min_right: 3
max_consecutive_hyphenated_lines: 2
avoid_proper_names_when_possible: true
avoid_after_short_lines: true
paragraphs:
first_paragraph_indent: "0"
indent: "1em"
block_paragraph_spacing: "0.6em"
headings:
keep_with_next_lines: 2
avoid_stranded_headings: true
numbering:
enabled: true
style: "decimal"
require_monotonic_increase: true
widows_orphans:
widow_lines: 2
orphan_lines: 2
balance_facing_pages: false
code:
inline:
use_mono: true
block:
font_size: "9.5pt"
line_height: 1.35
wrap: true
max_wrap_penalty: "medium"
overflow_policy: "wrap_then_shrink_minor"
shrink_limit: 0.92
tables:
cell_padding: "3pt 6pt"
header_repeat: true
overflow_policy: "shrink_then_wrap"
shrink_limit: 0.9
severity_overrides:
- selector: { category: "layout", tag: "widows_orphans" }
severity: "should"
- selector: { category: "accessibility" }
severity: "must"
locale_defaults:
primary_language: "en"
fallback_languages: ["fr"]
quotation_style: "us"
date_format: "YYYY-MM-DD"
number_format:
decimal_separator: "."
thousands_separator: ","

132
spec/quality_gates.yaml Normal file
View file

@ -0,0 +1,132 @@
version: "0.1.0"
description: >
Post-render QA gates. All thresholds are hard numeric limits used to fail builds
(unless a gate is explicitly marked as warn-only by the invoking CLI flags).
metrics:
max_widows_per_10_pages: "Count of widow lines across any 10 consecutive pages."
max_orphans_per_10_pages: "Count of orphan lines across any 10 consecutive pages."
max_stranded_headings: "Count of headings at page bottom with insufficient following content per keep rule."
max_overfull_lines: "Count of lines exceeding measure by overflow threshold (render-time measured)."
max_table_overflow_incidents: "Count of tables that overflow page/column bounds or are clipped."
max_code_overflow_incidents: "Count of code blocks with horizontal overflow or clipping."
max_link_wrap_incidents: "Count of wrapped URLs/DOIs/emails violating link wrap policy."
max_heading_numbering_errors: "Count of numbering sequence/format violations."
max_citation_format_errors: "Count of citations not matching configured style format."
overflow_detection:
overfull_line_threshold_css_px: 1.0
consider_clipping_as_overflow: true
ignore_decorative_elements: true
profiles:
web_pdf:
default:
max_widows_per_10_pages: 1
max_orphans_per_10_pages: 1
max_stranded_headings: 0
max_overfull_lines: 2
max_table_overflow_incidents: 0
max_code_overflow_incidents: 1
max_link_wrap_incidents: 2
max_heading_numbering_errors: 0
max_citation_format_errors: 0
strict:
max_widows_per_10_pages: 0
max_orphans_per_10_pages: 0
max_stranded_headings: 0
max_overfull_lines: 0
max_table_overflow_incidents: 0
max_code_overflow_incidents: 0
max_link_wrap_incidents: 0
max_heading_numbering_errors: 0
max_citation_format_errors: 0
print_pdf:
default:
max_widows_per_10_pages: 0
max_orphans_per_10_pages: 0
max_stranded_headings: 0
max_overfull_lines: 0
max_table_overflow_incidents: 0
max_code_overflow_incidents: 0
max_link_wrap_incidents: 0
max_heading_numbering_errors: 0
max_citation_format_errors: 0
strict:
max_widows_per_10_pages: 0
max_orphans_per_10_pages: 0
max_stranded_headings: 0
max_overfull_lines: 0
max_table_overflow_incidents: 0
max_code_overflow_incidents: 0
max_link_wrap_incidents: 0
max_heading_numbering_errors: 0
max_citation_format_errors: 0
dense_tech:
default:
max_widows_per_10_pages: 1
max_orphans_per_10_pages: 1
max_stranded_headings: 0
max_overfull_lines: 3
max_table_overflow_incidents: 1
max_code_overflow_incidents: 2
max_link_wrap_incidents: 3
max_heading_numbering_errors: 0
max_citation_format_errors: 0
strict:
max_widows_per_10_pages: 0
max_orphans_per_10_pages: 0
max_stranded_headings: 0
max_overfull_lines: 1
max_table_overflow_incidents: 0
max_code_overflow_incidents: 0
max_link_wrap_incidents: 1
max_heading_numbering_errors: 0
max_citation_format_errors: 0
memo:
default:
max_widows_per_10_pages: 3
max_orphans_per_10_pages: 3
max_stranded_headings: 0
max_overfull_lines: 2
max_table_overflow_incidents: 1
max_code_overflow_incidents: 1
max_link_wrap_incidents: 4
max_heading_numbering_errors: 1
max_citation_format_errors: 1
strict:
max_widows_per_10_pages: 1
max_orphans_per_10_pages: 1
max_stranded_headings: 0
max_overfull_lines: 0
max_table_overflow_incidents: 0
max_code_overflow_incidents: 0
max_link_wrap_incidents: 2
max_heading_numbering_errors: 0
max_citation_format_errors: 0
slide_deck:
default:
max_widows_per_10_pages: 5
max_orphans_per_10_pages: 5
max_stranded_headings: 0
max_overfull_lines: 0
max_table_overflow_incidents: 0
max_code_overflow_incidents: 0
max_link_wrap_incidents: 0
max_heading_numbering_errors: 0
max_citation_format_errors: 1
strict:
max_widows_per_10_pages: 2
max_orphans_per_10_pages: 2
max_stranded_headings: 0
max_overfull_lines: 0
max_table_overflow_incidents: 0
max_code_overflow_incidents: 0
max_link_wrap_incidents: 0
max_heading_numbering_errors: 0
max_citation_format_errors: 0

View file

@ -0,0 +1,4 @@
{"id":"HOUSE.A11Y.HEADINGS.NO_SKIPS","title":"Heading hierarchy must not skip levels","source_refs":["HOUSE §A11Y.BASICS p2"],"category":"accessibility","severity":"must","applies_to":"all","rule_text":"Do not skip heading levels (e.g., H2 directly to H4). Maintain a coherent heading hierarchy to support navigation and assistive tech.","rationale":"Skipped levels break document structure for readers and tools.","enforcement":"lint","autofix":"suggest","autofix_notes":"Report heading-level skips and suggest the nearest valid level based on surrounding context.","tags":["accessibility","headings"],"keywords":["heading hierarchy","a11y","structure"],"dependencies":[],"exceptions":["If a source document is intentionally flat (no headings), treat as degraded-mode and require manual structure decisions."],"status":"active"}
{"id":"HOUSE.A11Y.IMAGES.ALT_REQUIRED","title":"Images must have alt text (or explicit empty alt when decorative)","source_refs":["HOUSE §A11Y.BASICS p2"],"category":"accessibility","severity":"must","applies_to":"html","rule_text":"All images must include alt text; if an image is decorative, use an explicit empty alt so assistive tech can skip it.","rationale":"Alt text enables non-visual readers to understand content.","enforcement":"lint","autofix":"suggest","autofix_notes":"Warn on images without alt text and suggest adding a concise description or empty alt.","tags":["accessibility","images"],"keywords":["alt text","images","screen readers"],"dependencies":[],"exceptions":["For purely decorative background images, empty alt is preferred."],"status":"active"}
{"id":"HOUSE.A11Y.LINK_TEXT.DESCRIPTIVE","title":"Links should have meaningful text","source_refs":["HOUSE §A11Y.BASICS p2"],"category":"accessibility","severity":"should","applies_to":"all","rule_text":"Avoid generic link text (“here”, “this”) and ensure link labels convey destination or action.","rationale":"Meaningful links improve navigation for assistive tech users.","enforcement":"lint","autofix":"suggest","autofix_notes":"Warn on low-information link text and suggest a descriptive label.","tags":["accessibility","links"],"keywords":["link text","accessibility"],"dependencies":["HOUSE.LINKS.TEXT.DESCRIPTIVE"],"exceptions":["In UI components with adjacent context, short link text may be acceptable if the context is explicitly included."],"status":"active"}
{"id":"HOUSE.A11Y.DOCUMENT_LANGUAGE.DECLARE","title":"Declare document language when exporting HTML","source_refs":["HOUSE §A11Y.BASICS p2"],"category":"accessibility","severity":"should","applies_to":"html","rule_text":"Declare the primary language of the document (and mark language changes when feasible) to support correct pronunciation and hyphenation.","rationale":"Language metadata improves accessibility and typesetting quality.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Set <html lang> for HTML outputs based on profile locale defaults; warn when mixed-language segments are detected without marking.","tags":["accessibility","i18n"],"keywords":["lang attribute","hyphenation","screen readers"],"dependencies":[],"exceptions":["Short foreign phrases may not require explicit lang marking if they do not affect pronunciation or hyphenation."],"status":"active"}

View file

@ -0,0 +1,16 @@
{"id":"CMOS.CITATIONS.SYSTEM.CONSISTENT_CHOICE","title":"Choose one citation system and apply it consistently","source_refs":["CMOS18 §13 p775"],"category":"citations","severity":"must","applies_to":"all","rule_text":"Choose an established citation system (e.g., notes/bibliography or author-date) and apply it consistently within the document; do not mix systems without an explicit editorial decision.","rationale":"Consistency is the baseline for auditability and reader trust.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_system","manual_checklist=true"],"keywords":["notes and bibliography","author-date","citation system"],"dependencies":[],"exceptions":["Collected volumes may contain mixed styles across chapters; require an explicit house decision and clear boundaries."],"status":"active"}
{"id":"CMOS.CITATIONS.NOTES_BIBLIO.FIRST_NOTE.FULL_REFERENCE","title":"In notes/bibliography, provide a full citation on first mention in a note","source_refs":["CMOS18 §13 p783"],"category":"citations","severity":"must","applies_to":"all","rule_text":"In notes/bibliography style, the first citation of a source in a note should provide sufficient publication details for identification (author, title, publication facts, and locator where relevant).","rationale":"Full first-note citations let readers identify sources without guesswork.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_style=notes_bibliography","manual_checklist=true"],"keywords":["first note","full citation","notes and bibliography"],"dependencies":[],"exceptions":["Short-form citations may be acceptable only when a complete bibliography entry is immediately available and policy allows it."],"status":"active"}
{"id":"CMOS.CITATIONS.NOTES_BIBLIO.SUBSEQUENT_NOTES.SHORT_FORM","title":"In notes/bibliography, use a consistent short form for subsequent notes","source_refs":["CMOS18 §13.32 p790 (scan p812)"],"category":"citations","severity":"should","applies_to":"all","rule_text":"After the first full note for a source, use a consistent shortened form for subsequent notes (e.g., author + short title + locator) per the chosen style policy.","rationale":"Short forms reduce redundancy while maintaining traceability.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_style=notes_bibliography","manual_checklist=true"],"keywords":["short form","subsequent notes","short title"],"dependencies":[],"exceptions":["If shortened forms could be ambiguous across multiple works by the same author, expand the short form or add distinguishing details."],"status":"active"}
{"id":"CMOS.CITATIONS.NOTES_BIBLIO.BIBLIOGRAPHY.INCLUDE_WHEN_USED","title":"Include a bibliography when using notes/bibliography style (when required by the deliverable)","source_refs":["CMOS18 §13.65 p809 (scan p831)"],"category":"citations","severity":"should","applies_to":"all","rule_text":"When the deliverable calls for notes/bibliography style, include a bibliography/reference list per the style and scope of the work; treat this as a project requirement, not an afterthought.","rationale":"A bibliography is the primary navigational index for sources.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_style=notes_bibliography","manual_checklist=true"],"keywords":["bibliography","reference list","notes"],"dependencies":[],"exceptions":["Some short formats (brief memos) may omit the bibliography by design; document the omission explicitly."],"status":"active"}
{"id":"CMOS.CITATIONS.NOTES_BIBLIO.BIBLIOGRAPHY.SORT_BY_AUTHOR","title":"Sort bibliography entries consistently (typically by author)","source_refs":["CMOS18 §13.69 p816 (scan p838)"],"category":"citations","severity":"must","applies_to":"all","rule_text":"Sort bibliography entries consistently, typically alphabetically by author (or by title when no author is present), and apply the same ordering rules throughout.","rationale":"Sorting enables fast verification and reduces reviewer friction.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_style=notes_bibliography","manual_checklist=true"],"keywords":["bibliography order","alphabetical"],"dependencies":[],"exceptions":["Legal citation collections may require jurisdiction-first ordering; follow the governing standard for that corpus."],"status":"active"}
{"id":"CMOS.CITATIONS.NOTES_BIBLIO.NAME_ORDER.NOTES_VS_BIBLIO","title":"Use appropriate name order for notes vs bibliography","source_refs":["CMOS18 §13 p819"],"category":"citations","severity":"should","applies_to":"all","rule_text":"In notes, present author names in natural order; in bibliography entries, invert the first-listed author name (surname first) per the chosen citation style.","rationale":"This aligns with common indexing expectations and improves lookup.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_style=notes_bibliography","manual_checklist=true"],"keywords":["author name order","inverted name","bibliography"],"dependencies":[],"exceptions":["Corporate authors and organizations may follow different inversion conventions; use the styles guidance and consistency."],"status":"active"}
{"id":"CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.PARENTHETICAL","title":"In author-date style, include author and year in the text citation","source_refs":["CMOS18 §13 p833"],"category":"citations","severity":"must","applies_to":"all","rule_text":"In author-date style, include the authors surname and the year of publication in the in-text citation; include a locator (e.g., page) when quoting or citing a specific passage.","rationale":"Author-date citations are only useful when they can be matched to the reference list.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_style=author_date","manual_checklist=true"],"keywords":["author-date","in-text citation","year","page number"],"dependencies":[],"exceptions":["Some standards bodies define their own in-text formats; follow the governing standard when applicable."],"status":"active"}
{"id":"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.REQUIRED","title":"In author-date style, include a reference list and ensure in-text citations resolve to it","source_refs":["CMOS18 §13 p837"],"category":"citations","severity":"must","applies_to":"all","rule_text":"In author-date style, include a reference list and ensure every in-text citation has a corresponding entry; treat missing entries as a correctness failure.","rationale":"A reference list is the verification surface for author-date citations.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_style=author_date","manual_checklist=true"],"keywords":["reference list","author-date","resolve citations"],"dependencies":[],"exceptions":["If the deliverable is intentionally citation-light, avoid author-date style and use an alternative scheme appropriate to the format."],"status":"active"}
{"id":"CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.ORDER_AND_YEAR","title":"Format author-date reference entries consistently (including year placement)","source_refs":["CMOS18 §13 p837"],"category":"citations","severity":"should","applies_to":"all","rule_text":"Format author-date reference list entries consistently, including placement of the year and required publication fields, according to the chosen style policy.","rationale":"Consistent entry formatting prevents review churn and improves traceability.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citation_style=author_date","manual_checklist=true"],"keywords":["reference list format","year placement","author-date"],"dependencies":[],"exceptions":["If an entry lacks a date, use the styles guidance for undated works and be consistent."],"status":"active"}
{"id":"CMOS.CITATIONS.QUOTATIONS.LOCATORS.PAGE_REQUIRED","title":"Include page/locator information for direct quotations and specific claims","source_refs":["CMOS18 §13 p775"],"category":"citations","severity":"should","applies_to":"all","rule_text":"When quoting directly or pointing to a specific passage, include a locator (page, section, or equivalent) suitable to the source type.","rationale":"Locators are necessary for verifiable checking.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","locators"],"keywords":["page number","locator","quotation"],"dependencies":[],"exceptions":["Web sources without stable pagination may require section anchors or archived snapshots."],"status":"active"}
{"id":"CMOS.CITATIONS.DOI.PREFERRED_OVER_URL","title":"Prefer durable identifiers (DOI) over bare URLs when available","source_refs":["CMOS18 §13.7 p778 (scan p800)"],"category":"citations","severity":"should","applies_to":"all","rule_text":"When a durable identifier such as a DOI is available, prefer it over a bare URL; represent identifiers as stable HTTPS links per the chosen house policy.","rationale":"Durable identifiers reduce link rot and make citations easier to validate.","enforcement":"lint","autofix":"suggest","autofix_notes":"Suggest converting DOI strings into https://doi.org/ form and flagging unstable redirect/shortener URLs; do not rewrite without confirmation.","tags":["doi","links"],"keywords":["DOI","doi.org","persistent identifier"],"dependencies":[],"exceptions":["Some sources (legal docs, standards) do not use DOIs; cite their official publication link instead."],"status":"active"}
{"id":"CMOS.CITATIONS.ONLINE.ACCESS_DATE.WHEN_NEEDED","title":"Record access dates for online sources when required by policy","source_refs":["CMOS18 §13.15 p782 (scan p804)"],"category":"citations","severity":"should","applies_to":"all","rule_text":"When citing online material that is likely to change or lacks a stable publication record, record an access date according to the chosen citation policy.","rationale":"Access dates help explain discrepancies when sources change.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["online_sources","manual_checklist=true"],"keywords":["access date","online source","versioned web"],"dependencies":[],"exceptions":["Stable, versioned archives may allow omitting access dates if the version identifier is captured."],"status":"active"}
{"id":"CMOS.CITATIONS.LEGAL_PUBLIC_DOCS.USE_JURISDICTIONAL_FORMAT","title":"Use appropriate formats for legal and public documents","source_refs":["CMOS18 §14 p935"],"category":"citations","severity":"should","applies_to":"all","rule_text":"For legal and public documents, follow an appropriate jurisdictional format and include identifiers sufficient for retrieval (jurisdiction, issuing body, document number, date, and locator).","rationale":"Public document citations must be independently retrievable.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["legal","public_documents","manual_checklist=true"],"keywords":["legal citation","public documents","jurisdiction"],"dependencies":[],"exceptions":["If another standard governs the citation style for the corpus (e.g., Bluebook), treat Chicago pointers as contextual and follow the governing standard."],"status":"active"}
{"id":"CMOS.CITATIONS.IBID.MINIMIZE_OR_AVOID","title":"Avoid relying on ibid-style backreferences unless the policy requires it","source_refs":["CMOS18 §13.37 p791 (scan p813)"],"category":"citations","severity":"warn","applies_to":"all","rule_text":"Avoid making traceability depend on ibid-style backreferences when citations are processed or reflowed; prefer forms that remain unambiguous when notes are reordered.","rationale":"Ibid-style references can break under reformatting and automated processing.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","ibid"],"keywords":["ibid","short form","note processing"],"dependencies":[],"exceptions":["If the deliverable explicitly requires ibid, enforce it consistently and ensure notes are not programmatically reordered."],"status":"active"}
{"id":"CMOS.CITATIONS.TITLES.CAPITALIZATION.CONSISTENT","title":"Use consistent title capitalization policy within citations","source_refs":["CMOS18 §13 p775"],"category":"citations","severity":"should","applies_to":"all","rule_text":"Use a consistent title capitalization policy within citations (headline-style vs sentence-style) according to the selected style and locale; apply it uniformly.","rationale":"Inconsistent capitalization is a common audit distraction.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","capitalization"],"keywords":["headline style","sentence style","capitalization"],"dependencies":[],"exceptions":["If titles are quoted verbatim from a catalog record, preserve their casing and note the source."],"status":"active"}
{"id":"CMOS.CITATIONS.MATCHING.BIBLIO_ENTRY_REQUIRED","title":"Ensure citations resolve to a full entry when the format requires it","source_refs":["CMOS18 §13 p775"],"category":"citations","severity":"must","applies_to":"all","rule_text":"Where the chosen citation format requires a full entry (bibliography or reference list), ensure that each cited work resolves to exactly one corresponding entry.","rationale":"Unresolvable citations undermine verification and credibility.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","resolution"],"keywords":["resolve citations","bibliography entries","reference list entries"],"dependencies":[],"exceptions":["Some short formats may allow citations without full entries; treat this as an explicit project decision."],"status":"active"}

View file

@ -0,0 +1,45 @@
{"id": "CMOS.CITATIONS.NOTES.NOTE_MARKERS.SUPERSCRIPT_TEXT", "title": "Use superscript note markers in running text", "source_refs": ["CMOS18 \u00a713.27 p787 (scan p809)"], "category": "citations", "severity": "should", "applies_to": "md", "rule_text": "Use superscript note markers in running text and keep the note numbers in the notes list; avoid inline bracketed numbers in narrative prose when a footnote system is in use.", "rationale": "Superscripts separate citations from the sentence flow.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect bracketed or inline numeric markers in prose and suggest converting to Markdown footnotes.", "tags": ["notes", "note_markers"], "keywords": ["note markers", "superscript", "footnotes"], "dependencies": [], "exceptions": ["Legal or technical styles that require bracketed numbers should be handled by profile."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.NOTE_MARKERS.SEQUENCE_CONTINUOUS", "title": "Keep note numbers sequential within scope", "source_refs": ["CMOS18 \u00a713.28 p788 (scan p810)"], "category": "citations", "severity": "must", "applies_to": "md", "rule_text": "Number notes sequentially within the chosen scope (whole document or chapter) and do not restart or skip numbers without a clear boundary.", "rationale": "Sequential numbering preserves traceability.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Flag gaps, duplicates, or unexpected resets in note numbering.", "tags": ["notes", "note_markers", "sequence"], "keywords": ["note numbers", "sequence", "footnotes"], "dependencies": [], "exceptions": ["Appendixes or per-chapter notes may restart if labeled clearly."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.NOTE_MARKERS.PLACEMENT_AFTER_PUNCT", "title": "Place note markers after related punctuation", "source_refs": ["CMOS18 \u00a713.29 p788 (scan p810)"], "category": "citations", "severity": "should", "applies_to": "md", "rule_text": "Place note markers after the punctuation that ends the referenced clause or sentence, not before it.", "rationale": "Consistent placement improves readability.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "When a note marker precedes terminal punctuation, suggest moving it after the punctuation.", "tags": ["notes", "note_markers", "punctuation"], "keywords": ["note marker placement", "footnote position"], "dependencies": [], "exceptions": ["Markers tied to a specific word may follow that word immediately."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.NOTE_MARKERS.HEADINGS_END", "title": "Place note markers at the end of headings", "source_refs": ["CMOS18 \u00a713.30 p789 (scan p811)"], "category": "citations", "severity": "should", "applies_to": "md", "rule_text": "When a heading or title needs a note, place the marker at the end of the heading rather than inserting it mid-phrase.", "rationale": "Mid-heading markers disrupt scanning.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Suggest moving inline note markers in headings to the end of the heading.", "tags": ["notes", "headings"], "keywords": ["heading notes", "title notes"], "dependencies": [], "exceptions": ["Markers that cite a specific word in the heading may remain inline with editorial approval."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.MULTIPLE_CITATIONS.SINGLE_NOTE", "title": "Combine multiple citations into one note when possible", "source_refs": ["CMOS18 \u00a713.31 p789 (scan p811)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When several sources support the same statement, prefer a single note that contains multiple citations rather than multiple note markers in the text.", "rationale": "Fewer markers reduce visual clutter.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "manual_checklist=true"], "keywords": ["multiple citations", "note consolidation"], "dependencies": [], "exceptions": ["Separate notes may be needed when commentary differs per source."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.SHORT_FORM.BASIC_ELEMENTS", "title": "Short-form notes should include author, short title, and locator", "source_refs": ["CMOS18 \u00a713.33 p790 (scan p812)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Use a consistent short-form note that includes author (or editor), a shortened title, and a locator when available.", "rationale": "Short forms remain identifiable only with key elements.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "short_form", "manual_checklist=true"], "keywords": ["short form", "short title", "locator"], "dependencies": [], "exceptions": ["If a bibliography entry is immediately adjacent, a shorter form may be acceptable by policy."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.SHORT_FORM.CROSS_REFERENCE", "title": "Use cross-references to full notes when needed", "source_refs": ["CMOS18 \u00a713.34 p790 (scan p812)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When a source has a full note elsewhere, you may cross-reference that note rather than repeating full details; ensure the reference is unambiguous.", "rationale": "Cross-references reduce redundancy without losing traceability.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "short_form", "manual_checklist=true"], "keywords": ["cross-reference", "see note", "short form"], "dependencies": [], "exceptions": ["Do not cross-reference if notes will be reordered or filtered in output."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.CHAPTER_IN_EDITED_BOOK", "title": "Cite chapters in edited volumes with chapter and book details", "source_refs": ["CMOS18 \u00a713.25 p787 (scan p809)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "For a chapter or essay in an edited book, cite the chapter author and title plus the book title, editor, and relevant page range.", "rationale": "Chapter citations must be traceable to both the chapter and the host volume.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "manual_checklist=true"], "keywords": ["chapter citation", "edited book", "page range"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.JOURNAL.ARTICLE_ELEMENTS", "title": "Journal article citations must include journal, volume, issue, and pages", "source_refs": ["CMOS18 \u00a713.26 p787 (scan p809)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Cite journal articles with the journal title, volume and issue (or equivalent), year, and page range or locator.", "rationale": "These elements uniquely identify the article.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "manual_checklist=true"], "keywords": ["journal article", "volume", "issue", "page range"], "dependencies": [], "exceptions": ["Online-only journals without pagination should use stable locators or article IDs."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.QUOTE_IN_NOTE.LOCATOR", "title": "Quoted material inside notes still needs a locator", "source_refs": ["CMOS18 \u00a713.41 p794 (scan p816)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "If a note contains a direct quotation, include a locator for the quoted passage in that note.", "rationale": "Quoted content must remain verifiable even when placed in notes.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "manual_checklist=true", "locators"], "keywords": ["quotation in note", "locator", "page number"], "dependencies": [], "exceptions": ["Brief epigraph-style notes may be exempt if the source is immediately clear."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.SUBSTANTIVE.SEPARATE_FROM_SOURCE", "title": "Keep substantive note commentary distinct from source citation", "source_refs": ["CMOS18 \u00a713.42 p794 (scan p816)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When a note contains discursive commentary, keep the source citation clear and separate so the reader can identify it quickly.", "rationale": "Mixing commentary and citations obscures the source trail.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "manual_checklist=true"], "keywords": ["substantive notes", "commentary", "source citation"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.LONG_NOTES.PARAGRAPHING", "title": "Paragraph long notes to preserve readability", "source_refs": ["CMOS18 \u00a713.43 p794 (scan p816)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Long notes should be paragraph-separated rather than run as a single block.", "rationale": "Paragraphing reduces reader fatigue in note sections.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "manual_checklist=true"], "keywords": ["long notes", "paragraphing"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.FOOTNOTES.PAGE_BREAKS", "title": "Handle footnotes that break across pages cleanly", "source_refs": ["CMOS18 \u00a713.44 p794 (scan p816)"], "category": "citations", "severity": "should", "applies_to": "pdf", "rule_text": "When a footnote must break across pages, mark the continuation clearly and avoid splitting very short notes.", "rationale": "Clear continuation reduces reader confusion.", "enforcement": "postrender", "autofix": "reflow", "autofix_notes": "Adjust keep-with-next settings or reflow notes to avoid short splits; re-render and re-check.", "tags": ["footnotes", "pagination"], "keywords": ["footnote continuation", "page break"], "dependencies": [], "exceptions": ["Very dense pages may force a split; record in QA notes."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.FOOTNOTES_VS_ENDNOTES.CHOOSE", "title": "Choose footnotes or endnotes based on format and reader needs", "source_refs": ["CMOS18 \u00a713.46 p796 (scan p818)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Select footnotes or endnotes according to the format and reader workflow; do not mix without a clear rationale.", "rationale": "The note system affects usability and page makeup.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["notes", "manual_checklist=true"], "keywords": ["footnotes", "endnotes", "format choice"], "dependencies": [], "exceptions": ["Dual systems may be acceptable in critical editions if clearly labeled."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.ENDNOTES.PLACEMENT", "title": "Place endnotes at a consistent structural boundary", "source_refs": ["CMOS18 \u00a713.49 p797 (scan p819)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When using endnotes, place them consistently at the end of chapters or the end of the work, and label the section clearly.", "rationale": "Consistent placement improves navigability.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["endnotes", "manual_checklist=true"], "keywords": ["endnote placement", "chapter endnotes"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.ENDNOTES.RUNNING_HEADS", "title": "Use clear running heads for endnote sections when present", "source_refs": ["CMOS18 \u00a713.50 p797 (scan p819)"], "category": "citations", "severity": "should", "applies_to": "pdf", "rule_text": "If endnotes are grouped in a dedicated section, use running heads or section headers that identify the notes clearly.", "rationale": "Readers should know they are in the notes section.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["endnotes", "manual_checklist=true", "running_heads"], "keywords": ["endnotes", "running heads", "section headers"], "dependencies": [], "exceptions": ["Short documents may omit running heads."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.ENDNOTES.AVOID_IBID", "title": "Avoid ibid in endnotes when it reduces traceability", "source_refs": ["CMOS18 \u00a713.51 p800 (scan p822)"], "category": "citations", "severity": "warn", "applies_to": "all", "rule_text": "In endnotes, avoid ibid-style references that require readers to flip pages; prefer short forms that remain clear at a distance.", "rationale": "Endnotes are less convenient to navigate than footnotes.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["endnotes", "manual_checklist=true", "ibid"], "keywords": ["ibid", "endnotes", "short form"], "dependencies": [], "exceptions": ["If ibid is required by policy, keep endnotes short and stable."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.AUTHOR_DATE_PLUS_NOTES", "title": "Use author-date citations with notes for commentary when appropriate", "source_refs": ["CMOS18 \u00a713.53 p801 (scan p823)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "To reduce heavy annotation, consider author-date citations for sources and reserve notes for substantive commentary.", "rationale": "Separating sources and commentary keeps pages readable.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "author_date", "notes"], "keywords": ["author-date", "commentary notes", "hybrid system"], "dependencies": [], "exceptions": ["Projects that require full notes for sources should keep the notes system consistent."], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.UNNUMBERED.NOT_FOR_SOURCES", "title": "Use unnumbered notes only for non-source material", "source_refs": ["CMOS18 \u00a713.55 p801 (scan p823)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Unnumbered notes should be reserved for brief explanatory or acknowledgment material, not for source citations.", "rationale": "Source citations must be traceable and countable.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "notes"], "keywords": ["unnumbered notes", "acknowledgments"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.SOURCE_NOTES.REPRINTS", "title": "Cite original sources for reprints and previously published material", "source_refs": ["CMOS18 \u00a713.58 p805 (scan p827)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "For reprints or previously published material, include source notes that identify the original publication details.", "rationale": "Readers need the primary source to verify provenance.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "notes"], "keywords": ["reprints", "source notes", "original publication"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.NOTES.AVOID_OVERLONG", "title": "Avoid overlong notes; integrate lengthy commentary into the text", "source_refs": ["CMOS18 \u00a713.60 p807 (scan p829)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "If notes become long and discursive, rewrite or integrate the material into the main text and keep notes focused.", "rationale": "Overlong notes disrupt reading and layout.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "notes"], "keywords": ["overlong notes", "discursive notes"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.SAME_AUTHOR.ORDER", "title": "Order multiple works by the same author consistently", "source_refs": ["CMOS18 \u00a713.70 p816 (scan p838)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When listing multiple works by the same author, order them consistently (e.g., by title or by date) per the chosen policy.", "rationale": "Consistent ordering reduces lookup time.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["same author", "ordering", "bibliography"], "dependencies": [], "exceptions": ["Chronological ordering may be preferred in historical bibliographies."], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.REPEATED_NAMES.THREE_EM_DASH", "title": "Use a 3-em dash for repeated author names when allowed", "source_refs": ["CMOS18 \u00a713.73 p818 (scan p840)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When policy allows, replace repeated author names in consecutive bibliography entries with a 3-em dash; use it consistently or not at all.", "rationale": "Consistent repetition handling keeps lists compact and clear.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography", "repeated_names"], "keywords": ["3-em dash", "repeated names", "bibliography"], "dependencies": [], "exceptions": ["Some house styles require full names on every entry."], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.CONSISTENT_FORM", "title": "Use a consistent author name form across entries", "source_refs": ["CMOS18 \u00a713.74 p819 (scan p841)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Use a consistent form of the author name across all entries for that person.", "rationale": "Inconsistent name forms fragment an author's works.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["author name", "consistency"], "dependencies": [], "exceptions": ["If a name change is documented, include cross-references."], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.PUBLISHED_FORM", "title": "Respect the published form of an author's name", "source_refs": ["CMOS18 \u00a713.75 p819 (scan p841)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Use the form of the author's name as it appears in the source when practical, and do not invent expansions without evidence.", "rationale": "Preserving the published form reduces misattribution.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["author name", "published form"], "dependencies": [], "exceptions": ["If a house style requires standardization, document the choice."], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.AUTHOR_NAME.INITIALS_PREFERRED", "title": "Honor authors who publish under initials", "source_refs": ["CMOS18 \u00a713.76 p820 (scan p842)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "If an author is known to publish under initials, keep those initials rather than expanding to full names.", "rationale": "Respecting the author's preferred form avoids confusion.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["initials", "author name"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.MULTI_AUTHORS.ORDER", "title": "Preserve author order for multi-author works", "source_refs": ["CMOS18 \u00a713.78 p821 (scan p843)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "List multiple authors in the order given in the source and keep the formatting consistent.", "rationale": "Author order is part of the citation identity.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["multiple authors", "author order"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.SAME_SURNAME.DISAMBIGUATE", "title": "Disambiguate authors with the same surname", "source_refs": ["CMOS18 \u00a713.79 p821 (scan p843)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When different authors share a surname, include initials or full given names to disambiguate.", "rationale": "Disambiguation prevents misattribution.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["same surname", "disambiguation"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.NO_AUTHOR.TITLE_LEAD", "title": "Use the title to lead entries without authors", "source_refs": ["CMOS18 \u00a713.81 p822 (scan p844)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "If no author is listed, start the entry with the title and alphabetize by that title.", "rationale": "Title-led entries are the only way to locate anonymous works.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["anonymous works", "title lead"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.PSEUDONYMS.CONSISTENT", "title": "Treat pseudonyms as author names and use them consistently", "source_refs": ["CMOS18 \u00a713.82 p823 (scan p845)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Treat pseudonyms as the author name and use the same pseudonym form consistently across entries.", "rationale": "Consistency makes pseudonymous works discoverable.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["pseudonyms", "author name"], "dependencies": [], "exceptions": ["If a house style prefers real names, add a cross-reference."], "status": "active"}
{"id": "CMOS.CITATIONS.BIBLIOGRAPHY.ALT_NAMES.CROSSREF", "title": "Add cross-references for authors with multiple real names", "source_refs": ["CMOS18 \u00a713.84 p824 (scan p846)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When an author publishes under multiple real names, add cross-references so readers can connect the identities.", "rationale": "Cross-references prevent fragmentation across name variants.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "bibliography"], "keywords": ["name variants", "cross-reference"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.ALPHABETICAL", "title": "Alphabetize author-date reference list entries", "source_refs": ["CMOS18 \u00a713.112 p838 (scan p860)"], "category": "citations", "severity": "must", "applies_to": "all", "rule_text": "Arrange author-date reference list entries alphabetically by author (or by title when no author is present).", "rationale": "Alphabetical order is the primary access method for author-date lists.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "author_date", "reference_list"], "keywords": ["reference list", "alphabetical"], "dependencies": [], "exceptions": ["If a governing standard mandates a different order, document the override."], "status": "active"}
{"id": "CMOS.CITATIONS.AUTHOR_DATE.REFERENCE_LIST.REPEATED_NAMES", "title": "Handle repeated author names consistently in author-date lists", "source_refs": ["CMOS18 \u00a713.113 p838 (scan p860)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "If the style uses a repeated-name dash in reference lists, apply it consistently to consecutive entries by the same author(s).", "rationale": "Consistent repetition handling avoids list ambiguity.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "author_date", "reference_list"], "keywords": ["repeated names", "author-date"], "dependencies": [], "exceptions": ["Some house styles require repeating the full name."], "status": "active"}
{"id": "CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.LOCATORS", "title": "Include locators for specific passages in author-date citations", "source_refs": ["CMOS18 \u00a713.117 p840 (scan p862)"], "category": "citations", "severity": "must", "applies_to": "all", "rule_text": "When citing a specific passage in author-date style, include a locator such as a page or section number after the year.", "rationale": "Locators make claims verifiable.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "author_date", "locators"], "keywords": ["page numbers", "locator", "author-date"], "dependencies": [], "exceptions": ["Sources without stable pagination should use section or paragraph markers."], "status": "active"}
{"id": "CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.EXTRA_INFO", "title": "Add extra information to author-date citations only when needed", "source_refs": ["CMOS18 \u00a713.118 p841 (scan p863)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Add clarifying information inside author-date citations only when needed (e.g., edition, chapter, or other disambiguators).", "rationale": "Extra details should aid clarity, not clutter.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "author_date"], "keywords": ["disambiguation", "author-date"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.PLACEMENT", "title": "Place author-date citations where they least interrupt the sentence", "source_refs": ["CMOS18 \u00a713.119 p841 (scan p863)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Place author-date citations in a position that does not interrupt the sentence more than necessary, typically at the end of the relevant clause.", "rationale": "Better placement improves readability.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "author_date"], "keywords": ["citation placement", "author-date"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.DIRECT_QUOTES", "title": "Direct quotes in author-date style require a locator", "source_refs": ["CMOS18 \u00a713.120 p842 (scan p864)"], "category": "citations", "severity": "must", "applies_to": "all", "rule_text": "For direct quotations in author-date style, include a locator with the citation.", "rationale": "Quoted text must be verifiable.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "author_date", "locators"], "keywords": ["direct quote", "author-date", "locator"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.AUTHOR_DATE.IN_TEXT.MULTI_SOURCES", "title": "Separate multiple sources clearly in author-date citations", "source_refs": ["CMOS18 \u00a713.124 p844 (scan p866)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When listing multiple sources in one author-date citation, separate them clearly (e.g., with semicolons) and use a consistent order.", "rationale": "Clear separation avoids misreading combined citations.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "author_date"], "keywords": ["multiple sources", "author-date"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.ONLINE.URLS.STABLE", "title": "Use stable URLs for online sources", "source_refs": ["CMOS18 \u00a713.6 p778 (scan p800)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "For online sources, use stable URLs and avoid transient session or search-result links.", "rationale": "Stable URLs reduce link rot and aid verification.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["online_sources", "manual_checklist=true"], "keywords": ["URLs", "online sources", "link rot"], "dependencies": [], "exceptions": ["If no stable URL exists, include the best available URL and an access date."], "status": "active"}
{"id": "CMOS.CITATIONS.ONLINE.PERMALINKS.PREFERRED", "title": "Prefer permalinks or stable identifiers for online sources", "source_refs": ["CMOS18 \u00a713.8 p779 (scan p801)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Prefer permalinks or stable identifiers for online sources when available instead of temporary URLs.", "rationale": "Permalinks are more likely to survive over time.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["online_sources", "manual_checklist=true"], "keywords": ["permalink", "stable identifier"], "dependencies": [], "exceptions": ["When only a temporary URL is available, record an access date."], "status": "active"}
{"id": "CMOS.CITATIONS.ONLINE.VERSION.CITE_RIGHT_VERSION", "title": "Cite the specific version consulted for online sources", "source_refs": ["CMOS18 \u00a713.14 p782 (scan p804)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "When multiple versions exist, cite the specific version consulted and include version or edition information when available.", "rationale": "Versioning prevents disputes when content changes.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["online_sources", "manual_checklist=true"], "keywords": ["version", "online sources"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.ONLINE.REVISION_DATE.DISTINCT", "title": "Distinguish revision dates from access dates", "source_refs": ["CMOS18 \u00a713.16 p782 (scan p804)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "If a source provides a revision or last-modified date, treat it separately from the access date and include it when relevant.", "rationale": "Revision dates clarify which version was consulted.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["online_sources", "manual_checklist=true"], "keywords": ["revision date", "access date"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.RESEARCH.METADATA.CAPTURE_EARLY", "title": "Capture citation metadata during research", "source_refs": ["CMOS18 \u00a713.13 p781 (scan p803)"], "category": "citations", "severity": "should", "applies_to": "all", "rule_text": "Capture full citation metadata during research (author, title, publication facts, identifiers) rather than reconstructing it late.", "rationale": "Early capture prevents missing or incorrect details.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "research"], "keywords": ["metadata", "research", "citation management"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.CITATIONS.DEGRADED.NOTE_MARKERS.REPAIR", "title": "Repair degraded note markers during ingestion", "source_refs": ["CMOS18 \u00a713.27 p787 (scan p809)"], "category": "citations", "severity": "warn", "applies_to": "md", "rule_text": "When ingesting OCR or hard-wrapped text, normalize mangled note markers (e.g., split digits or bracketed numbers) into valid footnote syntax and flag ambiguous cases.", "rationale": "Degraded inputs often corrupt note markers and break resolution.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect likely footnote markers corrupted by line breaks or brackets and suggest repairs.", "tags": ["notes", "degraded_input", "note_markers"], "keywords": ["OCR", "hard wrap", "note markers"], "dependencies": [], "exceptions": ["Skip conversion inside code blocks or literal number lists."], "status": "active"}
{"id": "CMOS.CITATIONS.DEGRADED.URL_LINEBREAKS.NORMALIZE", "title": "Restore URLs broken by line wraps in citations", "source_refs": ["CMOS18 \u00a713.6 p778 (scan p800)"], "category": "citations", "severity": "warn", "applies_to": "md", "rule_text": "When citations include URLs broken by line wraps or soft hyphens, restore the URL before publication and flag uncertain repairs.", "rationale": "Broken URLs undermine source verification.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect soft-hyphen or line-broken URLs in citation contexts and suggest recombining.", "tags": ["online_sources", "degraded_input"], "keywords": ["broken URLs", "line breaks", "soft hyphen"], "dependencies": [], "exceptions": ["Do not rewrite URLs in code blocks or literal data extracts."], "status": "active"}

View file

@ -0,0 +1,4 @@
{"id":"HOUSE.CODE.INLINE.MONO_BACKTICKS","title":"Inline code should use backticks and monospace rendering","source_refs":["HOUSE §QA.CODE_OVERFLOW p2"],"category":"code","severity":"should","applies_to":"all","rule_text":"Render inline code as monospace and ensure it is delimited consistently (e.g., backticks in Markdown) so it is visually distinct from prose.","rationale":"Code tokens should be scannable and unambiguous.","enforcement":"lint","autofix":"suggest","autofix_notes":"Detect common inline-code patterns lacking backticks and suggest wrapping.","tags":["code"],"keywords":["inline code","monospace","backticks"],"dependencies":[],"exceptions":["Do not force backticks inside already-monospace contexts like code blocks."],"status":"active"}
{"id":"HOUSE.CODE.BLOCKS.LANGUAGE_TAGS.PREFERRED","title":"Code blocks should include a language tag when known","source_refs":["HOUSE §QA.CODE_OVERFLOW p2"],"category":"code","severity":"warn","applies_to":"md","rule_text":"Prefer language-tagged code fences (e.g., ```json) when the language is known; untagged blocks reduce syntax highlighting and reviewer comprehension.","rationale":"Language tags improve readability and tooling.","enforcement":"lint","autofix":"suggest","autofix_notes":"Warn on untagged code fences and suggest a language where it can be inferred safely (otherwise leave untagged).","tags":["code"],"keywords":["code fence","language tag","syntax highlighting"],"dependencies":[],"exceptions":["If language cannot be inferred safely, keep untagged."],"status":"active"}
{"id":"HOUSE.CODE.BLOCKS.NO_CLIPPING","title":"Code blocks must not clip; handle overflow explicitly","source_refs":["HOUSE §QA.CODE_OVERFLOW p2"],"category":"code","severity":"must","applies_to":"pdf","rule_text":"If a code block exceeds the measure, the renderer must wrap or apply bounded shrink per profile; it must never clip code.","rationale":"Clipped code is a correctness failure.","enforcement":"postrender","autofix":"reflow","autofix_notes":"If code overflow is detected, re-render using wrap-then-shrink policy within profile limits.","tags":["overflow","code"],"keywords":["code overflow","clipping","wrap"],"dependencies":[],"exceptions":["For print targets where wrapping harms meaning, allow a visible “scroll indicator” affordance if supported."],"status":"active"}
{"id":"HOUSE.CODE.BLOCKS.WRAP_POLICY","title":"Prefer wrapping long code lines for screen PDFs","source_refs":["HOUSE §QA.CODE_OVERFLOW p2"],"category":"code","severity":"should","applies_to":"pdf","rule_text":"For screen-first PDFs, prefer wrapping long code lines over horizontal scrolling; apply bounded shrink only as a secondary measure.","rationale":"Readers should not lose content off-screen.","enforcement":"typeset","autofix":"reflow","autofix_notes":"Apply profile token policy wrap_then_shrink_minor where available.","tags":["code","wrap"],"keywords":["code wrapping","screen pdf","overflow policy"],"dependencies":[],"exceptions":["For fixed-format standards, preserve lines when wrapping changes semantics and the profile allows it."],"status":"active"}

View file

@ -0,0 +1,12 @@
{"id":"BRING.HEADINGS.SUBHEADS.STYLE_CONTRIBUTES_TO_WHOLE","title":"Headings should contribute to the style of the whole","source_refs":["BRING §4.2 p65 (scan p64)"],"category":"headings","severity":"should","applies_to":"all","rule_text":"Choose heading forms (size, weight, case, alignment) that reinforce the documents overall typographic voice rather than fighting it.","rationale":"Headings are part of the page texture, not just labels.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Report when heading styles diverge sharply (e.g., mixed alignments/case patterns) and suggest normalizing to a small set of styles.","tags":["headings","consistency"],"keywords":["headings","subheads","hierarchy","style","consistency"],"dependencies":[],"exceptions":["Different heading styles may be appropriate for distinct document parts (e.g., appendices) if the hierarchy is explicit."],"status":"active"}
{"id":"BRING.HEADINGS.SUBHEADS.CROSSHEADS_SIDEHEADS.CHOOSE_DELIBERATELY","title":"Choose a consistent symmetric vs asymmetric heading scheme","source_refs":["BRING §4.2.1 p65 (scan p64)"],"category":"headings","severity":"should","applies_to":"all","rule_text":"Decide whether headings are primarily symmetric (centered) or asymmetric (sideheads), and keep the choice consistent across levels unless there is an explicit hierarchy reason to vary.","rationale":"Stable alignment choices reduce cognitive load.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Detect mixed heading alignments within the same level and suggest normalizing alignment per level.","tags":["headings","alignment"],"keywords":["crossheads","sideheads","alignment","centered","flush left"],"dependencies":[],"exceptions":["A single contrasting style can be used at the top or bottom of a heading hierarchy when applied consistently."],"status":"active"}
{"id":"BRING.HEADINGS.SUBHEADS.RIGHT_SIDEHEADS.VISIBILITY","title":"Right-aligned headings require strong visual emphasis","source_refs":["BRING §4.2.1 p65 (scan p64)"],"category":"headings","severity":"warn","applies_to":"all","rule_text":"If using right-aligned headings, ensure they have enough size/weight/spacing to be reliably noticed and do not disappear into the margin.","rationale":"Right-aligned one-line headings are easy to miss.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Flag right-aligned headings below a size/weight threshold relative to body text.","tags":["headings","alignment","visibility"],"keywords":["right aligned","heading visibility","weight","size"],"dependencies":[],"exceptions":["Short right-aligned headings can work as primary headings in generous layouts with clear hierarchy."],"status":"active"}
{"id":"BRING.HEADINGS.SUBHEADS.MARGIN_HEADS.RUNNING_SHOULDERHEADS","title":"Margin headings can replace size for prominence","source_refs":["BRING §4.2.1 p65 (scan p64)"],"category":"headings","severity":"should","applies_to":"all","rule_text":"When you need prominence without making headings large, consider shifting headings into the margin (margin heads) rather than escalating size and weight.","rationale":"Margin placement can create emphasis without disrupting rhythm.","enforcement":"typeset","autofix":"none","autofix_notes":"","tags":["headings","layout"],"keywords":["margin heads","running heads","shoulderheads"],"dependencies":[],"exceptions":["Margin heads require sufficient margin width and should not collide with running headers/footers."],"status":"active"}
{"id":"BRING.HEADINGS.SUBHEADS.LEVELS.AS_MANY_AS_NEEDED","title":"Use as many heading levels as needed, no more","source_refs":["BRING §4.2.1 p65 (scan p64)"],"category":"headings","severity":"should","applies_to":"all","rule_text":"Keep the number of heading levels to the minimum that expresses the documents structure; avoid inventing extra levels that are not meaningfully distinct.","rationale":"Over-deep hierarchies create noise and ambiguity.","enforcement":"lint","autofix":"suggest","autofix_notes":"Warn when heading depth exceeds a configured maximum for the profile (e.g., >4 levels) and suggest consolidation.","tags":["headings","hierarchy"],"keywords":["heading levels","structure","hierarchy depth"],"dependencies":[],"exceptions":["Long technical standards may require deeper hierarchies; prefer disambiguating labels over visual noise."],"status":"active"}
{"id":"BRING.HEADINGS.SUBHEADS.MIXING_SYMM_ASYMM.AVOID_HAPHAZARD","title":"Avoid haphazard mixing of symmetric and asymmetric subheads","source_refs":["BRING §4.2.2 p65 (scan p64)"],"category":"headings","severity":"should","applies_to":"all","rule_text":"If you mix symmetric and asymmetric subheads, do so systematically; avoid ad hoc alternation that creates both stylistic and logical confusion.","rationale":"Inconsistent heading geometry undermines hierarchy.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Detect alternating centered/flush styles within a single level and suggest a single dominant scheme.","tags":["headings","consistency"],"keywords":["subheads","symmetry","asymmetry","consistency"],"dependencies":[],"exceptions":["A small number of deliberate, consistently applied combinations can expand available hierarchy levels."],"status":"active"}
{"id":"BRING.HEADINGS.SUBHEADS.MIXING.HIERARCHY_PLACEMENT","title":"If mixing styles, place the contrast at hierarchy extremes","source_refs":["BRING §4.2.2 p65 (scan p64)"],"category":"headings","severity":"should","applies_to":"all","rule_text":"When adding a contrasting heading style into a series, prefer placing it at the top or bottom of the hierarchy rather than in the middle layers.","rationale":"Mid-hierarchy variation is easiest to misread.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","headings"],"keywords":["heading hierarchy","contrast","placement"],"dependencies":[],"exceptions":["If a middle layer must be distinct, use other channels (spacing, size, weight) consistently."],"status":"active"}
{"id":"CMOS.HEADINGS.RUNNING_HEADS.DEFINITION","title":"Define and use running heads consistently","source_refs":["CMOS18 §1.10 p8 (scan p30)"],"category":"headings","severity":"should","applies_to":"pdf","rule_text":"Treat running heads as navigational headings at the top of pages; keep their style and content consistent with the documents division and heading scheme.","rationale":"Running heads help readers orient in longer documents.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","running_heads"],"keywords":["running heads","navigation","page headers"],"dependencies":[],"exceptions":["Short memos and slide-like PDFs may omit running heads entirely."],"status":"active"}
{"id":"CMOS.HEADINGS.RUNNING_HEADS.NAVIGATION_SCOPE","title":"Running heads should help locate the right section","source_refs":["CMOS18 §1.10 p8 (scan p30)"],"category":"headings","severity":"should","applies_to":"pdf","rule_text":"If you include running heads, ensure they convey useful location context (e.g., section/chapter range) rather than repeating a generic label that provides no navigation value.","rationale":"Navigation elements should reduce search time.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","running_heads"],"keywords":["running heads","section context","navigation"],"dependencies":[],"exceptions":["Very short documents may use a single running head for branding rather than navigation."],"status":"active"}
{"id":"CMOS.HEADINGS.DIVISIONS.CHAPTERS.MULTIAUTHOR","title":"Multi-author chapter divisions need consistent chapter framing","source_refs":["CMOS18 §1.56 p33 (scan p55)"],"category":"headings","severity":"should","applies_to":"all","rule_text":"For multi-author works, use a consistent chapter division pattern so readers can recognize chapter boundaries and attribution placement.","rationale":"Consistent division framing prevents structural ambiguity.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","frontmatter"],"keywords":["multi-author","chapters","divisions","attribution"],"dependencies":[],"exceptions":["Single-author docs can keep chapter framing minimal if headings are unambiguous."],"status":"active"}
{"id":"CMOS.HEADINGS.DIVISIONS.LETTERS_DIARIES.HEADINGS","title":"Letters and diary entries use dates and signatures as headings","source_refs":["CMOS18 §1.58 p33 (scan p55)"],"category":"headings","severity":"should","applies_to":"all","rule_text":"When presenting letters or diary entries, treat dates and signatures (or both) as part of the heading structure and keep them consistently formatted.","rationale":"Document genres carry their own navigational conventions.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true"],"keywords":["letters","diaries","dates","headings"],"dependencies":[],"exceptions":["If entries are excerpted, note omissions explicitly rather than silently changing the heading scheme."],"status":"active"}
{"id":"HOUSE.HEADINGS.KEEPS.AVOID_STRANDED","title":"Avoid stranded headings via keep-with-next constraints","source_refs":["HOUSE §QA.KEEPS p1"],"category":"headings","severity":"must","applies_to":"pdf","rule_text":"A heading must not appear at the bottom of a page/column without sufficient following content; enforce keep-with-next rules at render time.","rationale":"Stranded headings harm readability and signal low-quality pagination.","enforcement":"postrender","autofix":"reflow","autofix_notes":"If a heading is stranded, reflow by moving the heading to the next page (or adjusting keeps within profile limits).","tags":["keep_constraints","widows_orphans"],"keywords":["keep with next","stranded heading","pagination"],"dependencies":[],"exceptions":["In narrow slide-like formats, a heading may be alone if it functions as a section break page."],"status":"active"}

View file

@ -0,0 +1,20 @@
{"id": "BRING.HEADINGS.STRUCTURE.MATCH_TEXT_LOGIC", "title": "Align heading hierarchy with text structure", "source_refs": ["BRING \u00a71.2.2 p20 (scan p19)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Make the heading hierarchy reflect the logical structure of the text rather than arbitrary styling.", "rationale": "Structural alignment improves navigation and comprehension.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings", "hierarchy"], "keywords": ["heading hierarchy", "structure"], "dependencies": [], "exceptions": ["Editorial or creative layouts may require intentional deviations."], "status": "active"}
{"id": "BRING.HEADINGS.RELATED_ELEMENTS.COHERENT", "title": "Keep headings coherent with related elements", "source_refs": ["BRING \u00a71.2.3 p21 (scan p20)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Keep headings, captions, notes, and other paratext in a coherent visual system so relationships are clear.", "rationale": "Coherent styling helps readers connect related elements.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings"], "keywords": ["paratext", "coherence"], "dependencies": [], "exceptions": ["Special sections may use a distinct style if the break is explicit."], "status": "active"}
{"id": "BRING.HEADINGS.DEGRADED.INFER_STRUCTURE", "title": "Infer heading structure cautiously in degraded inputs", "source_refs": ["BRING \u00a71.2.2 p20 (scan p19)"], "category": "headings", "severity": "warn", "applies_to": "md", "rule_text": "When headings are missing due to OCR or imported text, infer structure cautiously and flag uncertain cases for review.", "rationale": "Mis-inferred headings distort document structure.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Suggest heading inference based on typography or numbering and mark low-confidence cases.", "tags": ["degraded_input", "headings"], "keywords": ["OCR", "heading inference"], "dependencies": [], "exceptions": ["Do not infer headings inside code blocks or tables."], "status": "active"}
{"id": "BRING.HEADINGS.CAPITALIZATION.CONSISTENT", "title": "Use consistent capitalization within each heading level", "source_refs": ["BRING \u00a74.2 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "md", "rule_text": "Use a consistent capitalization style within each heading level (headline or sentence case).", "rationale": "Consistent case signals hierarchy and improves scanability.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect mixed capitalization styles within the same heading level and suggest normalization.", "tags": ["headings", "capitalization"], "keywords": ["heading case", "capitalization"], "dependencies": [], "exceptions": ["Quoted titles may preserve the source capitalization."], "status": "active"}
{"id": "BRING.HEADINGS.WEIGHT_SIZE.HIERARCHY_SCALE", "title": "Scale heading size and weight with hierarchy", "source_refs": ["BRING \u00a74.2 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Scale size and weight so higher-level headings read as more prominent than lower levels.", "rationale": "Clear scaling prevents hierarchy confusion.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Recommend a size and weight scale per heading level and validate in output.", "tags": ["headings", "hierarchy", "typeset"], "keywords": ["size", "weight", "hierarchy"], "dependencies": [], "exceptions": ["Short documents may compress the scale if levels are few."], "status": "active"}
{"id": "BRING.HEADINGS.SPACING.VERTICAL_RHYTHM", "title": "Keep heading spacing consistent", "source_refs": ["BRING \u00a74.2 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Maintain consistent spacing above and below headings to preserve vertical rhythm.", "rationale": "Consistent spacing improves scanability and rhythm.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Apply per-level spacing tokens and re-render to confirm consistent rhythm.", "tags": ["headings", "spacing", "typeset"], "keywords": ["spacing", "vertical rhythm"], "dependencies": [], "exceptions": ["Section breaks may intentionally use extra space."], "status": "active"}
{"id": "BRING.HEADINGS.STYLE.PALETTE_LIMIT", "title": "Limit the number of distinct heading styles", "source_refs": ["BRING \u00a74.2 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Limit the number of distinct heading styles and reuse a small palette across levels.", "rationale": "Too many styles reduce hierarchy clarity.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings"], "keywords": ["style palette", "heading styles"], "dependencies": [], "exceptions": ["Appendices or special sections may add a distinct style if labeled."], "status": "active"}
{"id": "BRING.HEADINGS.RUN_IN.STANDALONE.CONSISTENT", "title": "Choose run-in or stand-alone heads per level", "source_refs": ["BRING \u00a74.2.1 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Choose run-in or stand-alone headings per level and apply the choice consistently.", "rationale": "Consistent heading form improves navigation.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings"], "keywords": ["run-in heads", "stand-alone heads"], "dependencies": [], "exceptions": ["Short sections may use run-in heads for compactness."], "status": "active"}
{"id": "BRING.HEADINGS.ALIGNMENT.CONSISTENT_LEVEL", "title": "Keep alignment consistent within each heading level", "source_refs": ["BRING \u00a74.2.1 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Keep alignment consistent within a heading level (flush left, centered, or sidehead).", "rationale": "Consistent alignment reinforces hierarchy.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Detect mixed alignments within a level and suggest a single alignment per profile.", "tags": ["headings", "alignment", "typeset"], "keywords": ["alignment", "headings"], "dependencies": [], "exceptions": ["A deliberate contrast can be used for a single level if documented."], "status": "active"}
{"id": "BRING.HEADINGS.MARGIN_HEADS.CLEAR_GUTTER", "title": "Ensure margin heads have enough gutter space", "source_refs": ["BRING \u00a74.2.1 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Ensure margin or side heads have enough gutter space and do not collide with body text.", "rationale": "Crowded margins undermine legibility.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Adjust margin width or sidehead offset to avoid collisions.", "tags": ["headings", "margin_heads", "typeset"], "keywords": ["margin heads", "sideheads"], "dependencies": [], "exceptions": ["Very narrow formats may need a different heading style."], "status": "active"}
{"id": "BRING.HEADINGS.HIERARCHY.NO_SKIPPED_LEVELS", "title": "Avoid skipping heading levels", "source_refs": ["BRING \u00a74.2.1 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "md", "rule_text": "Do not skip heading levels (e.g., jump from H1 to H3) without structural justification.", "rationale": "Skipping levels confuses hierarchy and navigation.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect level jumps greater than one and flag for review.", "tags": ["headings", "hierarchy"], "keywords": ["skipped levels", "heading depth"], "dependencies": [], "exceptions": ["Legal or technical standards may mandate specific numbering."], "status": "active"}
{"id": "BRING.HEADINGS.CONTRAST.CLEAR_HIERARCHY", "title": "Ensure clear contrast between heading levels", "source_refs": ["BRING \u00a74.2.2 p65 (scan p64)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Ensure contrast between heading levels is clear and systematic across the document.", "rationale": "Clear contrast supports hierarchy recognition.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings"], "keywords": ["contrast", "hierarchy"], "dependencies": [], "exceptions": ["Minimalist designs may use subtle contrast but must remain legible."], "status": "active"}
{"id": "BRING.HEADINGS.PARAGRAPH_INDENT.AFTER_HEAD_NONE", "title": "Omit paragraph indents after headings", "source_refs": ["BRING \u00a72.3.2 p39 (scan p38)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Do not indent the paragraph immediately following a heading; use spacing to signal the break.", "rationale": "Indenting after a heading is redundant and can look cramped.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Remove first-line indent after headings and rely on heading spacing.", "tags": ["headings", "typeset"], "keywords": ["indentation", "after heading"], "dependencies": [], "exceptions": ["Run-in heads may require different treatment."], "status": "active"}
{"id": "BRING.HEADINGS.BLOCK_QUOTE.SPACING_AROUND", "title": "Maintain hierarchy when a section opens with a block quote", "source_refs": ["BRING \u00a72.3.3 p40 (scan p39)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "If a section opens with a block quote, adjust spacing so the heading and quote maintain a clear hierarchy.", "rationale": "Spacing keeps block quotes from overpowering headings.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Tune spacing between headings and block quotes to preserve hierarchy.", "tags": ["headings", "typeset"], "keywords": ["block quotes", "spacing"], "dependencies": [], "exceptions": ["Epigraph pages may intentionally feature large spacing differences."], "status": "active"}
{"id": "CMOS.HEADINGS.RUNNING_HEADS.LENGTH_SHORT", "title": "Keep running heads short", "source_refs": ["CMOS18 \u00a71.10 p8 (scan p30)"], "category": "headings", "severity": "should", "applies_to": "pdf", "rule_text": "Keep running heads short enough to fit comfortably with folios and avoid crowding.", "rationale": "Short heads maintain a clean page header.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "running_heads"], "keywords": ["running heads", "length"], "dependencies": [], "exceptions": ["Short documents may omit running heads entirely."], "status": "active"}
{"id": "CMOS.HEADINGS.RUNNING_HEADS.DIVISION_MATCH", "title": "Keep running heads aligned with the current division", "source_refs": ["CMOS18 \u00a71.10 p8 (scan p30)"], "category": "headings", "severity": "should", "applies_to": "pdf", "rule_text": "Update running heads to reflect the current division or section; avoid stale or generic heads.", "rationale": "Accurate running heads improve navigation.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "running_heads"], "keywords": ["running heads", "navigation"], "dependencies": [], "exceptions": ["Very short sections may reuse a higher-level head by design."], "status": "active"}
{"id": "CMOS.HEADINGS.MULTIAUTHOR.CHAPTER_NUMBERING", "title": "Use consistent chapter numbering in multi-author volumes", "source_refs": ["CMOS18 \u00a71.56 p33 (scan p55)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Use consistent chapter numbering and titling patterns across multi-author volumes.", "rationale": "Consistency helps readers recognize chapter boundaries.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings", "multi_author"], "keywords": ["chapter numbering", "multi-author"], "dependencies": [], "exceptions": ["Introductions or forewords may follow a different pattern if labeled."], "status": "active"}
{"id": "CMOS.HEADINGS.MULTIAUTHOR.AUTHOR_ATTRIBUTION_PLACEMENT", "title": "Place author attribution consistently in multi-author works", "source_refs": ["CMOS18 \u00a71.56 p33 (scan p55)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Place author names or affiliations in a consistent position relative to chapter headings.", "rationale": "Consistent attribution supports clear authorship cues.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings", "multi_author"], "keywords": ["author attribution", "chapter headings"], "dependencies": [], "exceptions": ["Contributed essays may require special attribution blocks."], "status": "active"}
{"id": "CMOS.HEADINGS.LETTERS_DIARIES.DATELINE_FORMAT", "title": "Format datelines consistently in letters and diaries", "source_refs": ["CMOS18 \u00a71.58 p33 (scan p55)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "In letters and diaries, format datelines consistently as part of the heading block.", "rationale": "Datelines function as navigational headings in these genres.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings"], "keywords": ["dateline", "letters", "diaries"], "dependencies": [], "exceptions": ["Excerpts may need contextual notes if the dateline is missing."], "status": "active"}
{"id": "CMOS.HEADINGS.LETTERS_DIARIES.SIGNATURE_FORMAT", "title": "Format signatures consistently in letters and diaries", "source_refs": ["CMOS18 \u00a71.58 p33 (scan p55)"], "category": "headings", "severity": "should", "applies_to": "all", "rule_text": "Keep signatures in letters and diary entries formatted consistently and distinct from body text.", "rationale": "Consistent signatures preserve genre conventions.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "headings"], "keywords": ["signatures", "letters", "diaries"], "dependencies": [], "exceptions": ["Some diary formats omit signatures by design."], "status": "active"}

View file

@ -0,0 +1,12 @@
{"id":"BRING.LAYOUT.MEASURE.COMFORTABLE_RANGE","title":"Choose a comfortable line length (measure)","source_refs":["BRING §2.1.2 p26"],"category":"layout","severity":"must","applies_to":"all","rule_text":"Set body text line length to a readable range; use a clear target band rather than leaving measure accidental. Treat narrow multi-column layouts as a separate measure regime.","rationale":"Line length strongly controls reading speed, fatigue, and hyphenation pressure.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Recommend a profile-specific max-width/column policy and validate against measured characters-per-line targets.","tags":["measure","line_length"],"keywords":["measure","line length","characters per line","columns"],"dependencies":[],"exceptions":["Some genres (tables, code) require different measures; handle via style tokens."],"status":"active"}
{"id":"BRING.LAYOUT.JUSTIFICATION.RAGGED_RIGHT_IF_NEEDED","title":"Prefer ragged-right when good spacing cannot be maintained","source_refs":["BRING §2.1.3 p27"],"category":"layout","severity":"should","applies_to":"all","rule_text":"If fully-justified text cannot maintain even spacing without visible artifacts, prefer a ragged-right setting over forcing justification that produces rivers and distortion.","rationale":"Uneven justification artifacts are more distracting than a well-managed rag.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Suggest switching justification mode or adjusting hyphenation/spacing parameters; requires rendered review.","tags":["justification","ragged_right"],"keywords":["ragged right","justified text","rivers"],"dependencies":[],"exceptions":["Print layouts with strict grids may accept tighter justification constraints if artifacts are controlled."],"status":"active"}
{"id":"BRING.LAYOUT.PAGINATION.ORPHANS_AVOID","title":"Avoid orphan lines at the top of a page","source_refs":["BRING §2-4.9 p44"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"Avoid starting a page with the last line of a multi-line paragraph; adjust pagination so paragraph continuity is preserved.","rationale":"Orphans interrupt comprehension by separating dependent lines.","enforcement":"postrender","autofix":"reflow","autofix_notes":"When detected, adjust keep/avoid-break settings or move a small amount of content across the page break; requires re-render.","tags":["widows_orphans","pagination"],"keywords":["orphan","page break","paragraph continuity"],"dependencies":[],"exceptions":["Very short pages (e.g., slides) may tolerate this if overflow constraints dominate."],"status":"active"}
{"id":"BRING.LAYOUT.PAGINATION.WIDOWS_AVOID","title":"Avoid widow lines at the bottom of a page","source_refs":["BRING §2-4.9 p44"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"Avoid leaving a single short last line of a paragraph isolated at the bottom of a page when it breaks the paragraphs rhythm; prefer reflow to keep at least a small run of lines together.","rationale":"Widows create visual and semantic discontinuity.","enforcement":"postrender","autofix":"reflow","autofix_notes":"When detected, adjust keep-with-next/avoid-break parameters or move content so paragraph lines balance across pages; requires re-render.","tags":["widows_orphans","pagination"],"keywords":["widow","page break","paragraph"],"dependencies":[],"exceptions":["In dense technical docs, strict overflow constraints may force occasional widows; record as QA warnings."],"status":"active"}
{"id":"BRING.LAYOUT.PAGINATION.BALANCE_FACING_PAGES","title":"Balance facing pages in book-style layouts","source_refs":["BRING §2-4.10 p44"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"In two-sided print layouts, prefer balancing facing pages by shifting small amounts of content rather than leaving large blank areas or uneven spreads.","rationale":"Balanced spreads improve perceived quality and reduce visual distraction.","enforcement":"postrender","autofix":"suggest","autofix_notes":"Suggest enabling facing-page balancing in print profiles; requires a paged-media aware renderer.","tags":["pagination","print_quality"],"keywords":["facing pages","spread","balance"],"dependencies":[],"exceptions":["Screen-first PDFs may disable spread balancing for predictability."],"status":"active"}
{"id":"BRING.LAYOUT.LINEBREAKS.AVOID_SAME_WORD_START","title":"Avoid starting consecutive lines with the same word","source_refs":["BRING §2-4.8 p43"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"Avoid line-break patterns that start multiple consecutive lines with the same word (especially in justified text); adjust breaks to reduce obvious repetition at the margin.","rationale":"Repeated line starts create visual ruts and reduce scanability.","enforcement":"postrender","autofix":"suggest","autofix_notes":"Suggest minor reflow (tracking/hyphenation tweaks) when detected; requires re-render to confirm improvement.","tags":["line_breaks","justification"],"keywords":["line break","consecutive lines","repeated word"],"dependencies":[],"exceptions":["Poetry or deliberate parallel structure may override this for meaning."],"status":"active"}
{"id":"BRING.LAYOUT.HYPHENATION.AVOID_NEAR_INTERRUPTION","title":"Avoid hyphen breaks near major interruptions (e.g., page breaks)","source_refs":["BRING §2-4.11 p44"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"Avoid hyphenating a word on a line adjacent to a major interruption (such as a page break) when it increases the sense of fragmentation; prefer alternative breaks when practical.","rationale":"Hyphens near hard breaks amplify the perception of chopped text.","enforcement":"postrender","autofix":"suggest","autofix_notes":"Suggest reflow or adjusting hyphenation penalties around breaks; requires paginated layout context.","tags":["hyphenation","pagination"],"keywords":["hyphenation","page break","interruption"],"dependencies":[],"exceptions":["Narrow measures may force hyphenation; record as QA warning when unavoidable."],"status":"active"}
{"id":"BRING.LAYOUT.HYPHENATION.STUB_END_AVOID","title":"Avoid leaving a very short fragment after hyphenation","source_refs":["BRING §2-4.3 p42"],"category":"layout","severity":"should","applies_to":"all","rule_text":"Avoid hyphenation patterns that leave an obviously tiny fragment at the end of a line or start of the next; prefer adjusting breaks or reflow.","rationale":"Stub fragments look accidental and reduce legibility.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Tune hyphenation settings (min fragment sizes, penalties) and confirm in rendering.","tags":["hyphenation"],"keywords":["hyphenation","stub","fragment"],"dependencies":[],"exceptions":["Some languages and narrow columns may require relaxing constraints."],"status":"active"}
{"id":"BRING.LAYOUT.RHYTHM.RULES_SERVE_TEXT","title":"Treat hyphenation/pagination rules as subordinate to readability","source_refs":["BRING §2-4.1 p42"],"category":"layout","severity":"warn","applies_to":"all","rule_text":"Use typographic rules to serve clarity, not to satisfy checklists: if a constraint consistently harms readability in a specific context, document the exception and apply a more appropriate profile.","rationale":"Rigid enforcement can produce worse output than a principled override.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","readability_override"],"keywords":["readability","exceptions","profiles"],"dependencies":[],"exceptions":["Overrides should be traceable (recorded in QA reports and profile diffs)."],"status":"active"}
{"id":"BRING.LAYOUT.MEASURE.MULTICOLUMN_TARGETS","title":"Use distinct measure targets for multi-column layouts","source_refs":["BRING §2.1.2 p26"],"category":"layout","severity":"should","applies_to":"all","rule_text":"When using multiple columns, use a tighter measure target than single-column body text; treat column count as a typographic decision, not a responsive afterthought.","rationale":"Column width affects hyphenation frequency and reading rhythm.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Recommend per-profile column and max-width targets; validate against measured characters-per-line constraints.","tags":["measure","columns"],"keywords":["columns","measure","responsive"],"dependencies":[],"exceptions":["Tables and code blocks may need full-width breaks or scrolling strategies."],"status":"active"}
{"id":"HOUSE.LAYOUT.OVERFLOW.OVERFULL_LINES.REPORT","title":"Detect and constrain overfull lines as a layout failure signal","source_refs":["HOUSE §QA.OVERFLOW p1"],"category":"layout","severity":"must","applies_to":"pdf","rule_text":"Treat overfull lines (content exceeding the intended measure) as a reportable failure; resolve via wrapping, reflow, or bounded font scaling rather than clipping.","rationale":"Overflow is a visible sign of broken layout constraints.","enforcement":"postrender","autofix":"reflow","autofix_notes":"When detected, attempt wrap-first strategies for code/URLs and minor font scaling within configured limits; re-render and re-check.","tags":["overflow","qa_gate"],"keywords":["overfull","overflow","clipping","wrapping"],"dependencies":[],"exceptions":["Some figures or long identifiers may require manual intervention; emit a checklist item when unresolved."],"status":"active"}
{"id":"HOUSE.LAYOUT.PAGINATION.KEEP_WITH_NEXT.HEADINGS","title":"Avoid stranded headings by keeping headings with following content","source_refs":["HOUSE §QA.KEEPS p1"],"category":"layout","severity":"must","applies_to":"pdf","rule_text":"Prevent headings from landing at the bottom of a page without sufficient following content; enforce keep-with-next policies for headings.","rationale":"Stranded headings break navigation and reduce perceived quality.","enforcement":"postrender","autofix":"reflow","autofix_notes":"Apply keep-with-next policies to headings and reflow pagination until headings meet the configured keep threshold.","tags":["keep_constraints","headings","pagination"],"keywords":["stranded heading","keep with next","pagination"],"dependencies":[],"exceptions":["Very short pages may relax keep thresholds via profile override; record in QA reports."],"status":"active"}

View file

@ -0,0 +1,30 @@
{"id": "BRING.LAYOUT.PAGE.FRAME.TEXTBLOCK_BALANCE", "title": "Frame the text block with balanced margins", "source_refs": ["BRING \u00a71.2.5 p23 (scan p22)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Shape the page so the text block is framed by balanced margins; avoid cramped margins that crowd the text.", "rationale": "Balanced margins improve readability and perceived quality.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Suggest margin and text block adjustments to achieve balanced framing.", "tags": ["layout", "margins", "typeset"], "keywords": ["margins", "text block"], "dependencies": [], "exceptions": ["Narrow formats may require tighter margins by necessity."], "status": "active"}
{"id": "BRING.LAYOUT.MARGINS.FACING_PAGES.INNER_OUTER", "title": "Balance inner and outer margins on facing pages", "source_refs": ["BRING \u00a71.2.5 p23 (scan p22)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "In facing-page layouts, keep inner and outer margins balanced to preserve the spread.", "rationale": "Balanced margins create stable spreads and comfortable gutters.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Adjust inner and outer margins to maintain consistent spread balance.", "tags": ["layout", "margins", "typeset"], "keywords": ["facing pages", "gutters"], "dependencies": [], "exceptions": ["Full-bleed designs may intentionally reduce margins."], "status": "active"}
{"id": "BRING.LAYOUT.ELEMENT_RELATIONSHIPS.VISIBLE", "title": "Make relationships between elements visually clear", "source_refs": ["BRING \u00a71.2.5 p23 (scan p22)"], "category": "layout", "severity": "should", "applies_to": "all", "rule_text": "Arrange headings, figures, notes, and tables so their relationship to the text is visually clear.", "rationale": "Clear relationships reduce reader confusion.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["layout", "manual_checklist=true"], "keywords": ["element relationships", "layout clarity"], "dependencies": [], "exceptions": ["Complex technical layouts may require explicit labels instead."], "status": "active"}
{"id": "BRING.LAYOUT.TEXTBLOCK.CONSISTENT_WIDTH", "title": "Keep text block width consistent within sections", "source_refs": ["BRING \u00a71.2.5 p23 (scan p22)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Keep the text block width consistent within a section; avoid jittery shifts in measure.", "rationale": "Consistent measure stabilizes reading rhythm.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Detect measure changes within a section and suggest a consistent width.", "tags": ["layout", "measure", "typeset"], "keywords": ["measure", "text block"], "dependencies": [], "exceptions": ["Sidebars and callouts may use a distinct measure by design."], "status": "active"}
{"id": "BRING.LAYOUT.FLOATS.PLACEMENT.NEAR_REFERENCE", "title": "Place floats near their first reference", "source_refs": ["BRING \u00a71.2.5 p23 (scan p22)"], "category": "layout", "severity": "should", "applies_to": "all", "rule_text": "Place figures and tables close to their first reference to keep reading flow intact.", "rationale": "Distant floats disrupt comprehension.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["layout", "manual_checklist=true"], "keywords": ["floats", "figures", "tables"], "dependencies": [], "exceptions": ["Complex layouts may require grouping figures in a dedicated section."], "status": "active"}
{"id": "BRING.LAYOUT.GRID.ALIGN_ELEMENTS", "title": "Align major elements to a common grid", "source_refs": ["BRING \u00a71.2.5 p23 (scan p22)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Align major elements to a common grid or baseline where practical to keep page texture coherent.", "rationale": "Grid alignment reduces visual jitter.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Use a baseline or layout grid and snap element positions to it.", "tags": ["layout", "grid", "typeset"], "keywords": ["grid", "baseline"], "dependencies": [], "exceptions": ["Illustrations may intentionally break the grid for emphasis."], "status": "active"}
{"id": "BRING.LAYOUT.MEASURE.TARGET_RANGE_CHARS", "title": "Aim for a readable line length", "source_refs": ["BRING \u00a72.1.2 p26 (scan p25)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Aim for a comfortable line length (roughly 45-75 characters per line) in body text.", "rationale": "Readable line length improves tracking and comprehension.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Suggest measure adjustments to keep lines within target range.", "tags": ["layout", "measure", "typeset"], "keywords": ["line length", "measure"], "dependencies": [], "exceptions": ["Narrow formats may require shorter lines."], "status": "active"}
{"id": "BRING.LAYOUT.MEASURE.ADJUST_FOR_TYPE_SIZE", "title": "Adjust measure when type size changes", "source_refs": ["BRING \u00a72.1.2 p26 (scan p25)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Adjust measure when type size changes; larger type generally needs a shorter line length.", "rationale": "Measure and type size should stay in balance.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Recommend measure changes based on body text size shifts.", "tags": ["layout", "measure", "typeset"], "keywords": ["type size", "measure"], "dependencies": [], "exceptions": ["Display typography may use different measures for effect."], "status": "active"}
{"id": "BRING.LAYOUT.MEASURE.AVOID_TOO_LONG", "title": "Avoid overly long lines", "source_refs": ["BRING \u00a72.1.2 p26 (scan p25)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Avoid overly long lines that make tracking to the next line difficult.", "rationale": "Long lines reduce reading speed and accuracy.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Shorten measure or increase column count when lines exceed target range.", "tags": ["layout", "measure", "typeset"], "keywords": ["long lines", "readability"], "dependencies": [], "exceptions": ["Wide tables or code blocks may need special handling."], "status": "active"}
{"id": "BRING.LAYOUT.MEASURE.AVOID_TOO_SHORT", "title": "Avoid overly short lines", "source_refs": ["BRING \u00a72.1.2 p26 (scan p25)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Avoid overly short lines that create choppy reading; consider columns or layout changes instead.", "rationale": "Very short lines disrupt reading rhythm.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Adjust measure or column count to avoid excessively short lines.", "tags": ["layout", "measure", "typeset"], "keywords": ["short lines", "columns"], "dependencies": [], "exceptions": ["Poetry and narrow marginalia may intentionally use short lines."], "status": "active"}
{"id": "BRING.LAYOUT.MEASURE.CONSISTENT_WITHIN_SECTION", "title": "Keep measure consistent within sections", "source_refs": ["BRING \u00a72.1.2 p26 (scan p25)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Keep measure consistent within a section unless a structural shift justifies a change.", "rationale": "Consistent measure stabilizes the reading experience.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Detect measure shifts within a section and recommend normalization.", "tags": ["layout", "measure", "typeset"], "keywords": ["measure consistency", "section layout"], "dependencies": [], "exceptions": ["Sidebars or callouts may intentionally vary measure."], "status": "active"}
{"id": "BRING.LAYOUT.MEASURE.CHANGE_FOR_LISTS", "title": "Adjust measure for lists and sidebars", "source_refs": ["BRING \u00a72.1.2 p26 (scan p25)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Adjust measure or indentation for lists and sidebars so they read as distinct structures.", "rationale": "Distinct measures signal structural changes.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Apply list and sidebar measure tokens separate from body text.", "tags": ["layout", "measure", "typeset"], "keywords": ["lists", "sidebars", "measure"], "dependencies": [], "exceptions": ["Very short lists may remain at body measure by design."], "status": "active"}
{"id": "BRING.LAYOUT.MEASURE.CODE_BLOCKS.WRAP_POLICY", "title": "Apply a consistent policy for code and long identifiers", "source_refs": ["BRING \u00a72.1.2 p26 (scan p25)"], "category": "layout", "severity": "should", "applies_to": "all", "rule_text": "Set a clear policy for code and long identifiers: wrap, scroll, or adjust measure consistently.", "rationale": "A consistent policy prevents ad hoc overflow fixes.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["layout", "manual_checklist=true"], "keywords": ["code blocks", "overflow"], "dependencies": [], "exceptions": ["Security-sensitive outputs may disable wrapping to preserve exact text."], "status": "active"}
{"id": "BRING.LAYOUT.COLUMNS.BALANCE_LENGTHS", "title": "Balance multi-column lengths", "source_refs": ["BRING \u00a72.1.2 p26 (scan p25)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "When using multiple columns, balance column lengths to avoid awkward white gaps.", "rationale": "Balanced columns improve visual cohesion.", "enforcement": "postrender", "autofix": "reflow", "autofix_notes": "Adjust column balance settings and re-render to reduce large gaps.", "tags": ["layout", "columns"], "keywords": ["columns", "balance"], "dependencies": [], "exceptions": ["Short columns may be acceptable for intentional design breaks."], "status": "active"}
{"id": "BRING.LAYOUT.LEADING.CHOOSE_BASE", "title": "Choose a base leading that suits the typeface and measure", "source_refs": ["BRING \u00a72.2.1 p36 (scan p35)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Choose a base leading that suits the typeface, text size, and measure.", "rationale": "Proper leading supports comfortable reading.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Recommend a leading value appropriate to the typeface and measure.", "tags": ["layout", "leading", "typeset"], "keywords": ["leading", "line spacing"], "dependencies": [], "exceptions": ["Display text may use tighter or looser leading for effect."], "status": "active"}
{"id": "BRING.LAYOUT.LEADING.CONSISTENT_BODY", "title": "Keep body text leading consistent", "source_refs": ["BRING \u00a72.2.1 p36 (scan p35)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Keep body text leading consistent within a section.", "rationale": "Consistent leading improves rhythm and appearance.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Detect inconsistent leading settings and suggest normalization.", "tags": ["layout", "leading", "typeset"], "keywords": ["leading", "consistency"], "dependencies": [], "exceptions": ["Sidebars may use a different leading by design."], "status": "active"}
{"id": "BRING.LAYOUT.LEADING.ADJUST_FOR_SIZE_CHANGES", "title": "Adjust leading when type size changes", "source_refs": ["BRING \u00a72.2.1 p36 (scan p35)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Adjust leading when type size changes so line spacing stays balanced.", "rationale": "Leading must scale with type size to remain legible.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Scale leading values with type size changes.", "tags": ["layout", "leading", "typeset"], "keywords": ["leading", "type size"], "dependencies": [], "exceptions": ["Captions may intentionally use tighter leading."], "status": "active"}
{"id": "BRING.LAYOUT.LEADING.AVOID_TOO_TIGHT", "title": "Avoid overly tight leading", "source_refs": ["BRING \u00a72.2.1 p36 (scan p35)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Avoid overly tight leading that causes lines to collide or create dark bands.", "rationale": "Tight leading reduces readability.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Increase leading when line collisions or dark bands appear.", "tags": ["layout", "leading", "typeset"], "keywords": ["tight leading", "readability"], "dependencies": [], "exceptions": ["Display text may use tighter leading as a design choice."], "status": "active"}
{"id": "BRING.LAYOUT.LEADING.AVOID_TOO_LOOSE", "title": "Avoid overly loose leading", "source_refs": ["BRING \u00a72.2.1 p36 (scan p35)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Avoid overly loose leading that breaks the text block and slows reading.", "rationale": "Loose leading weakens text cohesion.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Reduce leading if the text block loses cohesion.", "tags": ["layout", "leading", "typeset"], "keywords": ["loose leading", "text cohesion"], "dependencies": [], "exceptions": ["Generous leading may be acceptable in large-print editions."], "status": "active"}
{"id": "BRING.LAYOUT.LEADING.ALIGN_BASELINE_GRID", "title": "Align text to a baseline grid when used", "source_refs": ["BRING \u00a72.2.1 p36 (scan p35)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "When a baseline grid is used, align text and headings to it consistently.", "rationale": "Baseline grids create visual harmony across the page.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Snap text and heading baselines to the grid where possible.", "tags": ["layout", "leading", "typeset"], "keywords": ["baseline grid", "alignment"], "dependencies": [], "exceptions": ["Large display elements may intentionally break the grid."], "status": "active"}
{"id": "BRING.LAYOUT.PARAGRAPH.INDENT_AFTER_FIRST", "title": "Indent paragraphs after the first in continuous text", "source_refs": ["BRING \u00a72.3.2 p39 (scan p38)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "In continuous text, indent paragraphs after the first to signal new paragraphs.", "rationale": "Indentation is a clear paragraph cue.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Apply a consistent first-line indent to paragraphs after the first.", "tags": ["layout", "paragraphs", "typeset"], "keywords": ["paragraph indent", "continuous text"], "dependencies": [], "exceptions": ["Lists or block quotes may use different paragraph styling."], "status": "active"}
{"id": "BRING.LAYOUT.PARAGRAPH.INDENT_OR_SPACE_NOT_BOTH", "title": "Do not combine indents with extra paragraph spacing", "source_refs": ["BRING \u00a72.3.2 p39 (scan p38)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Use either paragraph indents or extra space between paragraphs, not both in the same context.", "rationale": "Combining both signals can look clumsy.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Choose one paragraph separation method per profile and normalize.", "tags": ["layout", "paragraphs", "typeset"], "keywords": ["paragraph spacing", "indents"], "dependencies": [], "exceptions": ["Section breaks may use extra space as a separator."], "status": "active"}
{"id": "BRING.LAYOUT.PARAGRAPH.NO_INDENT_AFTER_BLOCKS", "title": "Avoid indents after headings and display elements", "source_refs": ["BRING \u00a72.3.2 p39 (scan p38)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Do not indent the paragraph immediately following a heading, block quote, or other display element.", "rationale": "Indenting after a display element adds unnecessary noise.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Remove first-line indents following display elements.", "tags": ["layout", "paragraphs", "typeset"], "keywords": ["after heading", "after block quote"], "dependencies": [], "exceptions": ["Run-in heads may be treated differently."], "status": "active"}
{"id": "BRING.LAYOUT.PARAGRAPH.INDENT_SIZE.CONSISTENT", "title": "Keep paragraph indent size consistent", "source_refs": ["BRING \u00a72.3.2 p39 (scan p38)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Keep paragraph indent size consistent across body text.", "rationale": "Inconsistent indents disrupt rhythm.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Normalize indent size to the profile's setting.", "tags": ["layout", "paragraphs", "typeset"], "keywords": ["indent size", "consistency"], "dependencies": [], "exceptions": ["Special paragraphs may use a different indent by design."], "status": "active"}
{"id": "BRING.LAYOUT.PARAGRAPH.BLANK_LINES.SPARING", "title": "Use blank lines only for structural breaks", "source_refs": ["BRING \u00a72.3.2 p39 (scan p38)"], "category": "layout", "severity": "should", "applies_to": "all", "rule_text": "Avoid inserting blank lines between paragraphs in continuous text unless used as a structural break.", "rationale": "Excess blank lines fragment the text block.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["layout", "manual_checklist=true"], "keywords": ["blank lines", "paragraph breaks"], "dependencies": [], "exceptions": ["Screen-first content may use extra spacing for readability if consistent."], "status": "active"}
{"id": "BRING.LAYOUT.DEGRADED.HARD_WRAP_REFLOW", "title": "Reflow hard-wrapped paragraphs from degraded inputs", "source_refs": ["BRING \u00a72.3.2 p39 (scan p38)"], "category": "layout", "severity": "warn", "applies_to": "md", "rule_text": "When ingesting hard-wrapped text, reflow paragraphs to restore proper line breaks and spacing.", "rationale": "Hard wraps break reflow and create uneven text blocks.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect hard-wrapped paragraphs and suggest reflow into single paragraphs.", "tags": ["degraded_input", "layout"], "keywords": ["hard wraps", "reflow"], "dependencies": [], "exceptions": ["Do not reflow poetry or code blocks."], "status": "active"}
{"id": "BRING.LAYOUT.BLOCK_QUOTES.EXTRA_LEAD", "title": "Give block quotes extra spacing", "source_refs": ["BRING \u00a72.3.3 p40 (scan p39)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Give block quotations extra lead or spacing so they stand apart from body text.", "rationale": "Spacing distinguishes quotations from narrative.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Apply block-quote spacing tokens and confirm in output.", "tags": ["layout", "block_quotes", "typeset"], "keywords": ["block quotes", "spacing"], "dependencies": [], "exceptions": ["Short epigraphs may use custom spacing by design."], "status": "active"}
{"id": "BRING.LAYOUT.BLOCK_QUOTES.BEFORE_AFTER_SPACING", "title": "Use consistent spacing before and after block quotes", "source_refs": ["BRING \u00a72.3.3 p40 (scan p39)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Use consistent spacing before and after block quotes to maintain rhythm.", "rationale": "Consistent spacing keeps block quotes from interrupting flow.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Normalize block-quote spacing to the profile settings.", "tags": ["layout", "block_quotes", "typeset"], "keywords": ["block quotes", "spacing"], "dependencies": [], "exceptions": ["Design-led layouts may vary spacing for emphasis."], "status": "active"}
{"id": "BRING.LAYOUT.BLOCK_QUOTES.INDENT_OR_NARROW", "title": "Set block quotes with indentation or narrower measure", "source_refs": ["BRING \u00a72.3.3 p40 (scan p39)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Set block quotes with an indent or narrower measure rather than relying on quotation marks.", "rationale": "Distinct formatting signals quoted material.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Apply block-quote indentation or measure adjustments per profile.", "tags": ["layout", "block_quotes", "typeset"], "keywords": ["block quotes", "indentation"], "dependencies": [], "exceptions": ["Short quotations may remain inline instead of block quoted."], "status": "active"}
{"id": "BRING.LAYOUT.BLOCK_QUOTES.AVOID_CROWDING", "title": "Avoid crowding block quotes against headings", "source_refs": ["BRING \u00a72.3.3 p40 (scan p39)"], "category": "layout", "severity": "should", "applies_to": "pdf", "rule_text": "Avoid crowding block quotes against headings or captions; ensure clear separation.", "rationale": "Separation preserves hierarchy and readability.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Increase spacing between headings and block quotes to maintain hierarchy.", "tags": ["layout", "block_quotes", "typeset"], "keywords": ["block quotes", "headings"], "dependencies": [], "exceptions": ["Epigraph layouts may intentionally pair a heading and quote tightly."], "status": "active"}

View file

@ -0,0 +1,4 @@
{"id":"BRING.LAYOUT.VERTICAL_RHYTHM.MEASURED_INTERVALS.MULTIPLES","title":"Add and delete vertical space in measured intervals","source_refs":["BRING §2.2.2 p37 (scan p36)","BRING §2.2.2 p38 (scan p37)"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"When adding or removing vertical space (around headings, block quotes, figures, etc.), do it in measured intervals aligned to the documents basic leading/baseline grid so the text returns to the rhythm cleanly.","rationale":"Consistent vertical rhythm improves readability and page coherence.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Suggest spacing values as multiples of base leading to preserve vertical rhythm.","tags":["layout","vertical_rhythm","baseline_grid","typeset"],"keywords":["vertical rhythm","baseline grid","leading","spacing","headings"],"dependencies":["BRING.LAYOUT.LEADING.CHOOSE_BASE"],"exceptions":["Some sections (title pages, TOCs, display-heavy spreads) may intentionally break the rhythm."],"status":"active"}
{"id":"BRING.LAYOUT.LEADING.NEGATIVE.AVOID_CONTINUOUS_TEXT","title":"Avoid negative leading for continuous text","source_refs":["BRING §2.2.2 p37 (scan p36)"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"Avoid negative leading (line spacing tighter than the type size) for continuous text; prefer positive leading unless the face and setting have been tested for legibility.","rationale":"Overly tight leading reduces legibility and can cause collisions.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Detect excessively tight line-height and suggest increasing leading for body text.","tags":["layout","leading","typeset"],"keywords":["leading","line spacing","negative leading","legibility"],"dependencies":["BRING.LAYOUT.LEADING.CHOOSE_BASE"],"exceptions":["Display text may be set solid or tighter when legibility is preserved."],"status":"active"}
{"id":"BRING.LAYOUT.PAGE.DENSITY.DONT_SUFFOCATE","title":"Dont suffocate the page","source_refs":["BRING §2.2.3 p39 (scan p38)"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"Keep page density comfortable: avoid packing long lines with too many lines per page. Adjust measure, leading, or columns so the page can breathe.","rationale":"Overdense pages reduce comprehension and increase fatigue.","enforcement":"manual","autofix":"none","autofix_notes":"No deterministic autofix; flag dense pages for manual review or profile adjustment (measure/leading/columns).","tags":["layout","page_density","manual_checklist=true"],"keywords":["page density","measure","leading","columns","readability"],"dependencies":[],"exceptions":["Reference tables, data appendices, and indices may intentionally be denser than narrative text."],"status":"active"}
{"id":"BRING.LAYOUT.PARAGRAPH.OPENING_FLUSH_LEFT","title":"Set opening paragraphs flush left","source_refs":["BRING §2.3.1 p39 (scan p38)"],"category":"layout","severity":"should","applies_to":"pdf","rule_text":"Set the opening paragraph of a section flush left (no indent); use paragraph indents for subsequent paragraphs in continuous text.","rationale":"A flush-left opening avoids redundant indents and clarifies structure.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Detect the first paragraph after a heading and suggest removing its indent while keeping indents for subsequent paragraphs.","tags":["layout","paragraphs","indent","typeset"],"keywords":["opening paragraph","indent","flush left"],"dependencies":["BRING.LAYOUT.PARAGRAPH.INDENT_AFTER_FIRST"],"exceptions":["If the opening paragraph is preceded by a subhead or title that already provides separation, treat the choice as a house style decision."],"status":"active"}

View file

@ -0,0 +1,5 @@
{"id":"HOUSE.LINKS.TEXT.DESCRIPTIVE","title":"Link text should be descriptive (avoid bare URLs and “click here”)","source_refs":["HOUSE §A11Y.BASICS p2"],"category":"links","severity":"should","applies_to":"all","rule_text":"Prefer human-readable link text that describes the destination; avoid “click here” and avoid exposing long raw URLs in running prose when a label will do.","rationale":"Descriptive links improve comprehension and accessibility.","enforcement":"lint","autofix":"suggest","autofix_notes":"Warn on links with text like “click here” or raw URLs; suggest replacing with a descriptive label.","tags":["links","accessibility"],"keywords":["link text","click here","bare URL"],"dependencies":[],"exceptions":["In a references section, showing the full URL may be appropriate if formatting stays readable."],"status":"active"}
{"id":"HOUSE.LINKS.URLS.PREFER_HTTPS","title":"Prefer HTTPS URLs when available","source_refs":["HOUSE §QA.LINK_WRAP p2"],"category":"links","severity":"should","applies_to":"all","rule_text":"Prefer HTTPS links when an HTTPS equivalent exists; avoid plaintext HTTP for published documents unless the source is explicitly HTTP-only.","rationale":"HTTPS is the modern baseline for integrity and privacy.","enforcement":"lint","autofix":"suggest","autofix_notes":"Suggest upgrading http:// to https:// when the host is known to support HTTPS (manual confirmation may be required).","tags":["links"],"keywords":["https","urls","security"],"dependencies":[],"exceptions":["Do not rewrite URLs when the HTTPS target is unknown or changes meaning."],"status":"active"}
{"id":"HOUSE.LINKS.PUNCTUATION.NO_TRAILING_PUNCT","title":"Avoid trailing punctuation inside link targets","source_refs":["HOUSE §QA.LINK_WRAP p2"],"category":"links","severity":"must","applies_to":"all","rule_text":"Do not include trailing sentence punctuation (.,;:) inside link URLs; keep punctuation outside the link target.","rationale":"Trailing punctuation frequently breaks links.","enforcement":"lint","autofix":"rewrite","autofix_notes":"When a link target ends with a common punctuation character, move that punctuation outside the link.","tags":["links"],"keywords":["links","punctuation","trailing period"],"dependencies":[],"exceptions":["If the URL legitimately ends with punctuation, require manual confirmation."],"status":"active"}
{"id":"HOUSE.LINKS.WRAP.SAFE_BREAKS","title":"If a URL must wrap, break at safe separators","source_refs":["HOUSE §QA.LINK_WRAP p2"],"category":"links","severity":"should","applies_to":"pdf","rule_text":"When URLs wrap, prefer breaking at safe separators (/, -, ., ?, &, =) and avoid breaking the scheme/host portion where meaning is most fragile.","rationale":"Readable wrapping reduces copy/paste errors and verifier failures.","enforcement":"typeset","autofix":"reflow","autofix_notes":"Apply CSS/renderer line-break hints for URLs; post-render QA should flag wrapped URLs that violate policy.","tags":["links","wrap"],"keywords":["URL wrapping","line breaks","copy paste"],"dependencies":[],"exceptions":["In code blocks, URL wrapping rules follow code overflow policy instead."],"status":"active"}
{"id":"HOUSE.LINKS.DISALLOW.FILE_URIS","title":"Do not emit file:// links in published outputs","source_refs":["HOUSE §QA.LINK_WRAP p2"],"category":"links","severity":"must","applies_to":"all","rule_text":"Do not emit file:// URIs in output documents; use relative paths (for repo-local docs) or HTTPS URLs (for public assets).","rationale":"file:// links do not work for external readers and may leak local paths.","enforcement":"lint","autofix":"suggest","autofix_notes":"Warn on file:// links and suggest an HTTPS or relative alternative.","tags":["links","opsec"],"keywords":["file uri","opsec","links"],"dependencies":[],"exceptions":["Internal-only docs may use file:// for local workflows, but must be removed for publication."],"status":"active"}

View file

@ -0,0 +1,12 @@
{"id":"CMOS.NUMBERS.SPELLING.ONE_TO_ONE_HUNDRED.DEFAULT","title":"Spell out whole numbers in running text by default","source_refs":["CMOS18 §9.2 p590 (scan p612)"],"category":"numbers","severity":"should","applies_to":"all","rule_text":"In general prose, spell out whole numbers in a basic range (commonly one through one hundred) unless context favors numerals (measurements, statistics, dense technical material).","rationale":"Spelled-out numbers read more smoothly in narrative text.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","number_spelling"],"keywords":["spell out numbers","one through one hundred","numerals"],"dependencies":[],"exceptions":["Technical profiles may prefer numerals for consistency; document profile choice."],"status":"active"}
{"id":"CMOS.NUMBERS.NUMERALS.MEASUREMENTS.UNITS","title":"Use numerals with units of measure","source_refs":["CMOS18 §9.14 p597 (scan p619)","CMOS18 §9.18 p598 (scan p620)"],"category":"numbers","severity":"should","applies_to":"all","rule_text":"Use numerals with units of measure (including percentages and decimals) when presenting quantitative information; keep the unit formatting consistent.","rationale":"Numerals are faster to scan and reduce ambiguity in measurements.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["measurements","units","manual_checklist=true"],"keywords":["units of measure","percent","decimal"],"dependencies":[],"exceptions":["Narrative contexts may still spell out numbers when no unit is attached and readability benefits."],"status":"active"}
{"id":"CMOS.NUMBERS.DECIMALS.LEADING_ZERO","title":"Use a leading zero for decimals less than one","source_refs":["CMOS18 §9.21 p600 (scan p622)"],"category":"numbers","severity":"must","applies_to":"md","rule_text":"For decimal values less than one, include a leading zero before the decimal point (e.g., 0.5).","rationale":"A leading zero prevents misreading and improves accessibility.","enforcement":"lint","autofix":"rewrite","autofix_notes":"Rewrite occurrences of .N to 0.N in numeric contexts outside code blocks, avoiding version numbers and dotted identifiers where ambiguous.","tags":["decimals","leading_zero"],"keywords":["leading zero","decimal"],"dependencies":[],"exceptions":["Do not rewrite dotted identifiers (e.g., version strings) or code."],"status":"active"}
{"id":"CMOS.NUMBERS.GROUPING.THOUSANDS_SEPARATOR","title":"Use a consistent thousands separator for large numbers","source_refs":["CMOS18 §9.56 p612 (scan p634)"],"category":"numbers","severity":"should","applies_to":"all","rule_text":"Use a consistent thousands separator for numerals of four digits or more, according to the document locale/profile.","rationale":"Consistent grouping improves scanability and reduces errors.","enforcement":"lint","autofix":"suggest","autofix_notes":"Suggest inserting the configured separator (e.g., comma) for plain integers ≥ 1000 when unambiguous; do not rewrite IDs or code.","tags":["number_format","locale"],"keywords":["thousands separator","number grouping","locale"],"dependencies":[],"exceptions":["Identifiers, part numbers, and hashes should not be regrouped."],"status":"active"}
{"id":"CMOS.NUMBERS.PLURALS.DECADE.NO_APOSTROPHE","title":"Form plurals of decades without apostrophes","source_refs":["CMOS18 §9.55 p612 (scan p634)"],"category":"numbers","severity":"must","applies_to":"md","rule_text":"Form plurals of decades with a plain s (e.g., 1990s), not an apostrophe (1990s).","rationale":"Apostrophes signal possession, not pluralization.","enforcement":"lint","autofix":"rewrite","autofix_notes":"Rewrite common decade plural patterns like 1990s to 1990s outside code blocks.","tags":["plurals","decades"],"keywords":["1990s","decade plural","apostrophe"],"dependencies":[],"exceptions":["Possessive forms remain valid when intended (e.g., 1990s legacy meaning of 1990); require human review if ambiguous."],"status":"active"}
{"id":"CMOS.NUMBERS.RANGES.EN_DASH.USE","title":"Use en dashes for numeric ranges written in numerals","source_refs":["CMOS18 §9.62 p615 (scan p637)","CMOS18 §6.83 p415 (scan p437)"],"category":"numbers","severity":"should","applies_to":"md","rule_text":"When expressing a numeric range in numerals, use an en dash (e.g., 1015) rather than a hyphen; do not mix from/between with an en-dash range.","rationale":"This distinguishes ranges from hyphenation and reduces ambiguity.","enforcement":"lint","autofix":"suggest","autofix_notes":"Prefer the punctuation-level en-dash normalization; here the linter should flag mixed constructions like from 1015.","tags":["ranges","en_dash"],"keywords":["numeric range","en dash","from to"],"dependencies":["CMOS.PUNCTUATION.DASHES.EN_DASH.RANGES"],"exceptions":["Minus signs and negative ranges require careful handling; avoid automatic rewrites when a leading minus is present."],"status":"active"}
{"id":"CMOS.NUMBERS.ORDINALS.SUFFIX.CORRECT","title":"Use correct ordinal suffixes","source_refs":["CMOS18 §9.6 p592 (scan p614)"],"category":"numbers","severity":"should","applies_to":"md","rule_text":"When using ordinal numerals, use the correct suffix (e.g., 1st, 2nd, 3rd, 4th) and apply consistently.","rationale":"Incorrect ordinals look careless and reduce trust in numerical detail.","enforcement":"lint","autofix":"suggest","autofix_notes":"Flag common ordinal suffix errors (e.g., 1th) and suggest the correct form; do not rewrite automatically in ambiguous contexts.","tags":["ordinals"],"keywords":["ordinal","suffix","1st","2nd"],"dependencies":[],"exceptions":["Spelled-out ordinals (first, second) are acceptable; follow profile and context."],"status":"active"}
{"id":"CMOS.NUMBERS.INCLUSIVE_RANGES.PAGE_NUMBERS.SHORTEN","title":"Use consistent inclusive number ranges (page ranges)","source_refs":["CMOS18 §9.63 p616 (scan p638)"],"category":"numbers","severity":"should","applies_to":"all","rule_text":"For inclusive number ranges (especially page ranges), follow a consistent shortening rule appropriate to the style (e.g., avoid repeating unchanged leading digits when the context remains clear).","rationale":"Consistent inclusive ranges reduce visual clutter in citations and references.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["citations","page_ranges","manual_checklist=true"],"keywords":["inclusive numbers","page range","shortened range"],"dependencies":[],"exceptions":["When shortening would reduce clarity (e.g., across thousands boundaries), retain the full range."],"status":"active"}
{"id":"CMOS.NUMBERS.DATES.CONSISTENT_FORMAT","title":"Use a consistent date format per locale/profile","source_refs":["CMOS18 §9.31 p604 (scan p626)"],"category":"numbers","severity":"should","applies_to":"all","rule_text":"Use a consistent date format throughout the document according to locale and profile (e.g., ISO 8601 in technical specs, or month-day-year in US prose).","rationale":"Mixed date formats increase the risk of misinterpretation.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["dates","locale","manual_checklist=true"],"keywords":["date format","ISO 8601","month day year"],"dependencies":[],"exceptions":["Quoted material and data extracts may preserve source formatting; annotate if needed."],"status":"active"}
{"id":"CMOS.NUMBERS.CURRENCY.FORMAT.SYMBOL_PLACEMENT","title":"Format currency consistently","source_refs":["CMOS18 §9.22 p600 (scan p622)"],"category":"numbers","severity":"should","applies_to":"all","rule_text":"Format currency consistently: keep symbols and codes stable, avoid mixing symbol and code styles within the same document, and ensure grouping/decimals follow the locale profile.","rationale":"Consistent currency formatting avoids costly misunderstandings.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["currency","locale","manual_checklist=true"],"keywords":["currency","dollar","USD","EUR"],"dependencies":[],"exceptions":["Financial statements may require strict standards; follow the governing standard when applicable."],"status":"active"}
{"id":"CMOS.NUMBERS.SENTENCE_START.AVOID_NUMERAL","title":"Avoid starting a sentence with a numeral in prose contexts","source_refs":["CMOS18 §9.5 p591 (scan p613)"],"category":"numbers","severity":"should","applies_to":"all","rule_text":"In prose contexts, avoid starting a sentence with a numeral; rewrite the sentence or spell out the number to improve readability.","rationale":"Sentence-initial numerals can read awkwardly and hinder scanning.","enforcement":"manual","autofix":"suggest","autofix_notes":"Suggest rephrasing when a sentence begins with a numeral in body text, excluding list items and headings.","tags":["readability","manual_checklist=true"],"keywords":["sentence start","numeral"],"dependencies":[],"exceptions":["Lists and tabular contexts may begin with numerals; treat separately."],"status":"active"}
{"id":"CMOS.NUMBERS.CONSISTENCY.MIXED_FORMS.AVOID","title":"Avoid mixing spelled-out numbers and numerals within a comparable set","source_refs":["CMOS18 §9.2 p590 (scan p612)"],"category":"numbers","severity":"should","applies_to":"all","rule_text":"Within a comparable set (e.g., a list of quantities), avoid mixing spelled-out numbers and numerals without a clear reason; choose a consistent form.","rationale":"Mixed forms look arbitrary and reduce legibility.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["consistency","manual_checklist=true"],"keywords":["consistency","spelled out","numerals"],"dependencies":[],"exceptions":["Adjacent items with different contexts (e.g., one with units, one without) may justify different forms; consider rephrasing for uniformity."],"status":"active"}

View file

@ -0,0 +1,50 @@
{"id": "CMOS.NUMBERS.RULE_SELECTION.GENERAL_OR_ALTERNATIVE", "title": "Choose a primary number spelling rule", "source_refs": ["CMOS18 \u00a79.2 p590 (scan p612)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Choose either the general or alternative spelling rule for numbers in narrative text and apply it consistently within the document.", "rationale": "A single rule reduces distracting inconsistency.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["number_spelling", "consistency", "manual_checklist=true"], "keywords": ["general rule", "alternative rule", "number spelling"], "dependencies": [], "exceptions": ["Technical profiles may mandate numerals throughout."], "status": "active"}
{"id": "CMOS.NUMBERS.RULE_SELECTION.ALTERNATIVE_ZERO_TO_NINE", "title": "Alternative rule spells out zero through nine", "source_refs": ["CMOS18 \u00a79.3 p590 (scan p612)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "If using the alternative rule, spell out zero through nine and use numerals for 10 and above.", "rationale": "The alternative rule standardizes small-number spelling.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["number_spelling", "manual_checklist=true"], "keywords": ["zero through nine", "alternative rule"], "dependencies": [], "exceptions": ["Measurements and statistical contexts may still require numerals."], "status": "active"}
{"id": "CMOS.NUMBERS.ROUND_NUMBERS.SPELL_OUT", "title": "Spell out round numbers in narrative text", "source_refs": ["CMOS18 \u00a79.4 p591 (scan p613)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Spell out round numbers in the hundreds and thousands when the general rule applies; use numerals when precision or context demands.", "rationale": "Spelled-out round numbers read smoothly in prose.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["round_numbers", "manual_checklist=true"], "keywords": ["hundreds", "thousands", "round numbers"], "dependencies": [], "exceptions": ["Technical documents may prefer numerals for all quantities."], "status": "active"}
{"id": "CMOS.NUMBERS.DENSE_CONTEXT.USE_NUMERALS", "title": "Prefer numerals in number-dense passages", "source_refs": ["CMOS18 \u00a79.7 p592 (scan p614)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "In passages with many numbers or dense data, prefer numerals for readability even when some numbers could be spelled out.", "rationale": "Numerals are easier to scan in data-heavy text.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dense_numbers", "manual_checklist=true"], "keywords": ["dense data", "numerals"], "dependencies": [], "exceptions": ["Narrative sections may still favor spelled-out numbers for tone."], "status": "active"}
{"id": "CMOS.NUMBERS.CONTEXTS.ALWAYS_NUMERALS", "title": "Use numerals in conventionally numeric contexts", "source_refs": ["CMOS18 \u00a79.8 p593 (scan p615)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals in contexts that are conventionally numeric (measurements, percentages, decimals, numbered parts) unless a profile overrides.", "rationale": "Conventional numeric contexts benefit from compact notation.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["contexts", "manual_checklist=true"], "keywords": ["measurements", "percentages", "decimals"], "dependencies": [], "exceptions": ["Sentence-initial numbers may be spelled out by policy."], "status": "active"}
{"id": "CMOS.NUMBERS.LARGE_VALUES.MILLIONS_BILLIONS", "title": "Combine numerals with million/billion for large values", "source_refs": ["CMOS18 \u00a79.9 p595 (scan p617)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Combine numerals with words like million or billion for large round values when it improves clarity.", "rationale": "Mixed forms reduce long digit strings in prose.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["large_numbers", "manual_checklist=true"], "keywords": ["million", "billion", "large numbers"], "dependencies": [], "exceptions": ["Financial tables may require full numerals for precision."], "status": "active"}
{"id": "CMOS.NUMBERS.SCIENTIFIC.POWERS_OF_TEN", "title": "Use scientific notation for very large or small quantities", "source_refs": ["CMOS18 \u00a79.10 p596 (scan p618)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "For very large or very small quantities, use scientific notation or powers of ten when precision matters.", "rationale": "Scientific notation prevents misreading large magnitudes.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["scientific_notation", "manual_checklist=true"], "keywords": ["scientific notation", "powers of ten"], "dependencies": [], "exceptions": ["General audience prose may prefer rounded values."], "status": "active"}
{"id": "CMOS.NUMBERS.SI_PREFIXES.NO_HYPHEN", "title": "Write SI prefixes without hyphens", "source_refs": ["CMOS18 \u00a79.11 p596 (scan p618)"], "category": "numbers", "severity": "should", "applies_to": "md", "rule_text": "Write SI prefixes without a hyphen or space unless the unit convention requires it.", "rationale": "Standard SI forms reduce ambiguity.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Flag hyphenated SI prefix forms and suggest the standard unhyphenated form when safe.", "tags": ["si_prefixes", "number_format"], "keywords": ["SI prefixes", "hyphenation"], "dependencies": [], "exceptions": ["Follow the unit convention if a hyphen is standard for a specific term."], "status": "active"}
{"id": "CMOS.NUMBERS.BASES.NON_DECIMAL.NO_GROUPING", "title": "Avoid digit grouping in non-decimal bases", "source_refs": ["CMOS18 \u00a79.12 p596 (scan p618)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "For numbers in non-decimal bases (binary, octal, hexadecimal), avoid inserting thousands separators unless the style explicitly calls for it.", "rationale": "Grouping can obscure the base representation.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["number_format", "bases", "manual_checklist=true"], "keywords": ["binary", "hexadecimal", "digit grouping"], "dependencies": [], "exceptions": ["Some coding standards allow grouping for readability; document the choice."], "status": "active"}
{"id": "CMOS.NUMBERS.FRACTIONS.SIMPLE.SPELL_OUT", "title": "Spell out simple fractions in running text", "source_refs": ["CMOS18 \u00a79.15 p597 (scan p619)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Spell out simple fractions when they appear alone in running text.", "rationale": "Spelled-out simple fractions read naturally in prose.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["fractions", "manual_checklist=true"], "keywords": ["simple fractions", "spelled out"], "dependencies": [], "exceptions": ["Technical contexts may prefer numeric fractions."], "status": "active"}
{"id": "CMOS.NUMBERS.FRACTIONS.MIXED.WHOLE_PLUS_FRACTION", "title": "Format mixed numbers consistently", "source_refs": ["CMOS18 \u00a79.16 p598 (scan p620)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Express mixed numbers (whole number plus fraction) consistently and avoid ambiguous spacing.", "rationale": "Consistent mixed-number formatting reduces misreading.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["fractions", "manual_checklist=true"], "keywords": ["mixed fractions", "whole numbers"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.FRACTIONS.MATH.NUMERALS", "title": "Use numeric fractions in mathematical contexts", "source_refs": ["CMOS18 \u00a79.17 p598 (scan p620)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "In mathematical contexts, use numerals and explicit fraction formatting rather than spelled-out fractions.", "rationale": "Math contexts demand unambiguous numeric forms.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["fractions", "math", "manual_checklist=true"], "keywords": ["math fractions", "numeric fractions"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.UNITS.REPEATED.OMIT_REPEAT", "title": "Omit repeated units when quantities share a unit", "source_refs": ["CMOS18 \u00a79.19 p599 (scan p621)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "When listing multiple quantities that share a unit, omit repeating the unit if the grouping remains unambiguous.", "rationale": "Omitting redundant units reduces clutter.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["units", "manual_checklist=true"], "keywords": ["repeated units", "measurement"], "dependencies": [], "exceptions": ["Repeat the unit when there is any risk of ambiguity."], "status": "active"}
{"id": "CMOS.NUMBERS.PERCENTAGES.NUMERALS", "title": "Use numerals for percentages", "source_refs": ["CMOS18 \u00a79.20 p599 (scan p621)"], "category": "numbers", "severity": "should", "applies_to": "md", "rule_text": "Use numerals for percentages except when a sentence begins with the number.", "rationale": "Numerals make percentages easier to scan.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Flag spelled-out percentages and suggest numerals where safe.", "tags": ["percentages"], "keywords": ["percent", "percentage"], "dependencies": [], "exceptions": ["Sentence-initial percentages may be spelled out by policy."], "status": "active"}
{"id": "CMOS.NUMBERS.DECIMALS.NUMERALS", "title": "Use numerals for decimal fractions", "source_refs": ["CMOS18 \u00a79.21 p600 (scan p622)"], "category": "numbers", "severity": "should", "applies_to": "md", "rule_text": "Express decimal fractions as numerals rather than words in running text.", "rationale": "Decimal fractions are clearer in numeric form.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Flag spelled-out decimals and suggest numerals when unambiguous.", "tags": ["decimals"], "keywords": ["decimal", "numerals"], "dependencies": [], "exceptions": ["Do not rewrite inside code blocks or data extracts."], "status": "active"}
{"id": "CMOS.NUMBERS.CURRENCY.WORDS_VS_SYMBOLS", "title": "Use symbols for exact currency amounts", "source_refs": ["CMOS18 \u00a79.22 p600 (scan p622)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use currency symbols with numerals for exact amounts; reserve spelled-out currency for approximate or narrative contexts.", "rationale": "Symbols with numerals make exact amounts easier to verify.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["currency", "manual_checklist=true"], "keywords": ["currency symbols", "money"], "dependencies": [], "exceptions": ["Regulated financial documents may impose their own formats."], "status": "active"}
{"id": "CMOS.NUMBERS.CURRENCY.NON_US.DISAMBIGUATE", "title": "Disambiguate non-US dollar symbols", "source_refs": ["CMOS18 \u00a79.23 p601 (scan p623)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "When the dollar symbol represents a non-US currency, disambiguate with a currency code or context.", "rationale": "The dollar symbol is used by multiple currencies.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["currency", "manual_checklist=true"], "keywords": ["currency code", "non-US dollar"], "dependencies": [], "exceptions": ["If the context is unambiguous (e.g., a local-only report), note the assumption."], "status": "active"}
{"id": "CMOS.NUMBERS.CURRENCY.ISO_CODES", "title": "Use ISO codes or clear symbols for other currencies", "source_refs": ["CMOS18 \u00a79.25 p602 (scan p624)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "For other currencies, use ISO currency codes or clear symbols and keep the format consistent.", "rationale": "Standard codes prevent ambiguity across locales.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["currency", "manual_checklist=true"], "keywords": ["ISO codes", "currency"], "dependencies": [], "exceptions": ["Narrative prose may spell out the currency name if clarity remains high."], "status": "active"}
{"id": "CMOS.NUMBERS.CURRENCY.LARGE_AMOUNTS", "title": "Format large monetary amounts compactly", "source_refs": ["CMOS18 \u00a79.26 p602 (scan p624)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "For large monetary amounts, use numerals plus a clear unit (e.g., million) rather than long digit strings.", "rationale": "Compact forms reduce reading errors.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["currency", "manual_checklist=true"], "keywords": ["large amounts", "money"], "dependencies": [], "exceptions": ["Tables may require full digits for calculation."], "status": "active"}
{"id": "CMOS.NUMBERS.CURRENCY.HISTORICAL.YEAR_CONTEXT", "title": "Add year context for historical currency values", "source_refs": ["CMOS18 \u00a79.27 p602 (scan p624)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "When citing historical monetary values, include the year or period to anchor the amount.", "rationale": "Currency values change over time.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["currency", "manual_checklist=true", "historical"], "keywords": ["historical currency", "year"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.REFERENCES.PAGE_CHAPTER_FIGURE", "title": "Use numerals for references to pages, chapters, tables, and figures", "source_refs": ["CMOS18 \u00a79.28 p603 (scan p625)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals for references to pages, chapters, tables, and figures.", "rationale": "Numeric references are standard and easy to scan.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["references", "manual_checklist=true"], "keywords": ["page numbers", "chapter numbers", "figure numbers"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.PERIODICALS.VOLUME_ISSUE", "title": "Use numerals for periodical volume and issue numbers", "source_refs": ["CMOS18 \u00a79.29 p603 (scan p625)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals for periodical volume, issue, and page references.", "rationale": "Standard numbering aids citation lookup.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["periodicals", "manual_checklist=true"], "keywords": ["volume", "issue", "page numbers"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.DATES.YEAR_NUMERALS", "title": "Use numerals for years", "source_refs": ["CMOS18 \u00a79.31 p604 (scan p626)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Express years as numerals rather than spelling them out.", "rationale": "Year numerals are standard and concise.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dates", "manual_checklist=true"], "keywords": ["year", "dates"], "dependencies": [], "exceptions": ["Formal titles may preserve spelled-out years if required by the source."], "status": "active"}
{"id": "CMOS.NUMBERS.DATES.ABBREVIATED_YEAR", "title": "Use abbreviated years only when unambiguous", "source_refs": ["CMOS18 \u00a79.32 p604 (scan p626)"], "category": "numbers", "severity": "warn", "applies_to": "all", "rule_text": "Use abbreviated years only in informal contexts and ensure the abbreviation cannot be confused.", "rationale": "Abbreviated years are easy to misread.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dates", "manual_checklist=true"], "keywords": ["abbreviated year", "dates"], "dependencies": [], "exceptions": ["Internal shorthand is acceptable in limited-scope working notes."], "status": "active"}
{"id": "CMOS.NUMBERS.DATES.MONTH_DAY_STYLE", "title": "Use numerals for the day in month-day dates", "source_refs": ["CMOS18 \u00a79.33 p604 (scan p626)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "In month-day constructions, use numerals for the day and a spelled-out month name.", "rationale": "This format is standard and readable in prose.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dates", "manual_checklist=true"], "keywords": ["month-day", "date format"], "dependencies": [], "exceptions": ["Tables may use all-numeral dates for alignment."], "status": "active"}
{"id": "CMOS.NUMBERS.CENTURIES.SPELLED_OUT", "title": "Spell out centuries as ordinals", "source_refs": ["CMOS18 \u00a79.34 p605 (scan p627)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Spell out centuries as ordinals and use lowercase when referring to a century in running text.", "rationale": "This is the conventional prose form for centuries.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dates", "manual_checklist=true"], "keywords": ["centuries", "ordinals"], "dependencies": [], "exceptions": ["Headings may use numerals if the style requires it."], "status": "active"}
{"id": "CMOS.NUMBERS.DECADES.CONSISTENT_FORM", "title": "Use a consistent form for decades", "source_refs": ["CMOS18 \u00a79.35 p605 (scan p627)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Express decades consistently (spelled out or numerals) and avoid mixing forms within a document.", "rationale": "Mixed decade forms look inconsistent and confuse readers.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dates", "decades", "manual_checklist=true"], "keywords": ["decades", "consistency"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.ERAS.BCE_CE", "title": "Use a consistent era designation", "source_refs": ["CMOS18 \u00a79.36 p606 (scan p628)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use a consistent era designation (BCE/CE or BC/AD) with numerals across the document.", "rationale": "Consistent era markers prevent historical ambiguity.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dates", "eras", "manual_checklist=true"], "keywords": ["BCE", "CE", "era designations"], "dependencies": [], "exceptions": ["Quoted material may retain the original era markers."], "status": "active"}
{"id": "CMOS.NUMBERS.DATES.ALL_NUMERAL", "title": "Keep all-numeral dates consistent", "source_refs": ["CMOS18 \u00a79.37 p607 (scan p629)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "If using all-numeral dates, keep a single ordering and separator style throughout.", "rationale": "Mixed numeric date formats are easy to misread.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dates", "manual_checklist=true"], "keywords": ["numeric dates", "date order"], "dependencies": [], "exceptions": ["Imported data may preserve source formats but should be labeled."], "status": "active"}
{"id": "CMOS.NUMBERS.DATES.ISO_8601", "title": "Use ISO 8601 dates in technical contexts", "source_refs": ["CMOS18 \u00a79.38 p607 (scan p629)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "In technical or data-driven contexts, use ISO 8601 date format (YYYY-MM-DD) for clarity and sorting.", "rationale": "ISO dates sort lexicographically and reduce ambiguity.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["dates", "iso", "manual_checklist=true"], "keywords": ["ISO 8601", "date format"], "dependencies": [], "exceptions": ["Narrative prose may prefer spelled-out month names."], "status": "active"}
{"id": "CMOS.NUMBERS.TIME.GENERAL_NUMERALS", "title": "Use numerals for precise times of day", "source_refs": ["CMOS18 \u00a79.39 p607 (scan p629)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals for precise times of day; reserve spelled-out forms for informal references.", "rationale": "Numeric times are faster to scan and verify.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["time", "manual_checklist=true"], "keywords": ["time of day", "numerals"], "dependencies": [], "exceptions": ["Dialogue may follow conversational phrasing."], "status": "active"}
{"id": "CMOS.NUMBERS.TIME.NOON_MIDNIGHT", "title": "Prefer noon and midnight over ambiguous 12 a.m./p.m.", "source_refs": ["CMOS18 \u00a79.40 p608 (scan p630)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use noon and midnight instead of ambiguous 12 a.m./p.m. unless the 24-hour system is used.", "rationale": "Noon and midnight avoid ambiguity.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["time", "manual_checklist=true"], "keywords": ["noon", "midnight"], "dependencies": [], "exceptions": ["Data exports may require numeric times for consistency."], "status": "active"}
{"id": "CMOS.NUMBERS.TIME.TWENTY_FOUR_HOUR", "title": "Format 24-hour times consistently", "source_refs": ["CMOS18 \u00a79.41 p608 (scan p630)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "When using the 24-hour system, format times consistently and omit a.m./p.m.", "rationale": "Consistent 24-hour formatting improves clarity.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["time", "manual_checklist=true"], "keywords": ["24-hour time", "time format"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.TIME.ISO_STYLE", "title": "Use ISO time format when specified", "source_refs": ["CMOS18 \u00a79.42 p609 (scan p631)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "If using ISO time, include leading zeros and a consistent separator.", "rationale": "ISO time avoids ambiguity and sorts predictably.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["time", "iso", "manual_checklist=true"], "keywords": ["ISO time", "leading zeros"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.NAMES.MONARCHS_POPES", "title": "Use numerals for monarchs and similar titles", "source_refs": ["CMOS18 \u00a79.43 p609 (scan p631)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals for monarchs, popes, and similar ordinal titles in accordance with conventional naming.", "rationale": "Conventional naming aids recognition and indexing.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["names", "manual_checklist=true"], "keywords": ["monarchs", "popes", "ordinal titles"], "dependencies": [], "exceptions": ["Historical quotations may preserve the source's form."], "status": "active"}
{"id": "CMOS.NUMBERS.VEHICLES.VESSELS_NUMBERS", "title": "Use numerals for vehicle and vessel numbers", "source_refs": ["CMOS18 \u00a79.46 p610 (scan p632)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals for vehicle and vessel numbers when they are part of the name or designation.", "rationale": "Numerals are standard in equipment designations.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["names", "vehicles", "manual_checklist=true"], "keywords": ["vehicles", "vessels", "designations"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.PLACES.HIGHWAYS", "title": "Use numerals for highway designations", "source_refs": ["CMOS18 \u00a79.52 p611 (scan p633)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals for highway designations and follow local formatting conventions.", "rationale": "Highway numbering is a formal designation system.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["places", "manual_checklist=true"], "keywords": ["highways", "route numbers"], "dependencies": [], "exceptions": ["Spelled-out names may appear in historical titles."], "status": "active"}
{"id": "CMOS.NUMBERS.PLACES.STREETS", "title": "Use numerals for numbered streets and avenues", "source_refs": ["CMOS18 \u00a79.53 p611 (scan p633)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals for numbered streets and keep directional abbreviations consistent.", "rationale": "Numerical street names are standard identifiers.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["places", "manual_checklist=true"], "keywords": ["streets", "addresses"], "dependencies": [], "exceptions": ["Local style may spell out streets below a threshold; document the choice."], "status": "active"}
{"id": "CMOS.NUMBERS.PLACES.BUILDINGS_APTS", "title": "Use numerals for building and apartment numbers", "source_refs": ["CMOS18 \u00a79.54 p611 (scan p633)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use numerals for building and apartment numbers and keep the format consistent.", "rationale": "Numerals match real-world identifiers.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["places", "manual_checklist=true"], "keywords": ["building numbers", "apartment numbers"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.PLURALS.SPELLED_OUT", "title": "Form plurals of spelled-out numbers without apostrophes", "source_refs": ["CMOS18 \u00a79.55 p612 (scan p634)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Form plurals of spelled-out numbers as regular nouns without apostrophes.", "rationale": "Apostrophes are not used for simple plurals.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["plurals", "manual_checklist=true"], "keywords": ["plural numbers", "apostrophes"], "dependencies": [], "exceptions": ["Possessive forms remain valid when intended."], "status": "active"}
{"id": "CMOS.NUMBERS.DECIMAL_MARKER.LOCALE", "title": "Use the locale-appropriate decimal marker", "source_refs": ["CMOS18 \u00a79.57 p613 (scan p635)"], "category": "numbers", "severity": "should", "applies_to": "md", "rule_text": "Use the decimal marker appropriate to the locale (period or comma) and do not mix markers.", "rationale": "Mixed decimal markers cause numeric ambiguity.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Flag mixed decimal markers and suggest normalizing to the profile locale.", "tags": ["number_format", "locale"], "keywords": ["decimal marker", "locale"], "dependencies": [], "exceptions": ["Quoted or source-derived data may retain original markers with a note."], "status": "active"}
{"id": "CMOS.NUMBERS.DIGIT_GROUPING.SI_SPACE", "title": "Use SI digit grouping when configured", "source_refs": ["CMOS18 \u00a79.58 p613 (scan p635)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "In SI-number style contexts, group digits with thin spaces rather than commas, per the selected profile.", "rationale": "SI grouping aligns with international scientific norms.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Apply the profile's digit-grouping settings during typesetting; verify in output.", "tags": ["number_format", "si", "typeset"], "keywords": ["SI grouping", "thin space"], "dependencies": [], "exceptions": ["Locales that mandate comma grouping should follow the locale profile instead."], "status": "active"}
{"id": "CMOS.NUMBERS.TELEPHONE.FORMAT", "title": "Format telephone numbers consistently", "source_refs": ["CMOS18 \u00a79.59 p614 (scan p636)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Format telephone numbers with a consistent grouping pattern appropriate to the locale.", "rationale": "Consistent grouping improves readability and dialing accuracy.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["telephone", "manual_checklist=true"], "keywords": ["phone numbers", "grouping"], "dependencies": [], "exceptions": ["International numbers may follow ITU conventions for grouping."], "status": "active"}
{"id": "CMOS.NUMBERS.RATIOS.FORMAT", "title": "Express ratios consistently", "source_refs": ["CMOS18 \u00a79.60 p615 (scan p637)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Express ratios consistently using numerals with either a colon or words like to; avoid mixing styles.", "rationale": "Consistent ratio notation reduces ambiguity.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["ratios", "manual_checklist=true"], "keywords": ["ratios", "colon notation"], "dependencies": [], "exceptions": ["Mathematical formulas may require a specific notation."], "status": "active"}
{"id": "CMOS.NUMBERS.LISTS.OUTLINE.NUMERAL_STYLE", "title": "Use a consistent numeral system for outlines", "source_refs": ["CMOS18 \u00a79.61 p615 (scan p637)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use a consistent numbering system (arabic or roman) for outlines and numbered lists.", "rationale": "Consistent list numbering improves navigation.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["lists", "manual_checklist=true"], "keywords": ["numbered lists", "outline"], "dependencies": [], "exceptions": ["Nested outlines may use multiple numbering styles when clearly signaled."], "status": "active"}
{"id": "CMOS.NUMBERS.INCLUSIVE.COMMAS", "title": "Keep comma grouping consistent in inclusive ranges", "source_refs": ["CMOS18 \u00a79.65 p617 (scan p639)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "When inclusive numbers contain commas, apply the same grouping pattern to both ends of the range.", "rationale": "Consistent grouping prevents misreading ranges.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["inclusive_numbers", "manual_checklist=true"], "keywords": ["inclusive range", "comma grouping"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.INCLUSIVE.YEARS", "title": "Avoid ambiguous abbreviated year ranges", "source_refs": ["CMOS18 \u00a79.66 p617 (scan p639)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Abbreviate inclusive years only when the abbreviated form is unambiguous; otherwise use the full range.", "rationale": "Ambiguous year ranges lead to incorrect interpretation.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["inclusive_numbers", "manual_checklist=true"], "keywords": ["inclusive years", "year range"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.NUMBERS.ROMAN_NUMERALS.USE", "title": "Use roman numerals only in conventional contexts", "source_refs": ["CMOS18 \u00a79.67 p617 (scan p639)"], "category": "numbers", "severity": "should", "applies_to": "all", "rule_text": "Use roman numerals only in contexts where they are conventional and apply the style consistently.", "rationale": "Roman numerals can be slower to parse in general text.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["roman_numerals", "manual_checklist=true"], "keywords": ["roman numerals", "conventional use"], "dependencies": [], "exceptions": ["Source titles may preserve roman numerals as published."], "status": "active"}
{"id": "CMOS.NUMBERS.DEGRADED.NUMERAL_NORMALIZATION", "title": "Normalize common OCR numeral confusions", "source_refs": ["CMOS18 \u00a79.57 p613 (scan p635)"], "category": "numbers", "severity": "warn", "applies_to": "md", "rule_text": "In OCR or imported text, normalize common numeral confusions (O vs 0, l vs 1) in numeric contexts and flag uncertain cases.", "rationale": "OCR errors in numbers can change meaning.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect common OCR confusions in numeric tokens and suggest corrections with a review flag.", "tags": ["degraded_input", "number_format"], "keywords": ["OCR", "numeral normalization"], "dependencies": [], "exceptions": ["Do not rewrite inside code blocks or identifiers."], "status": "active"}
{"id": "CMOS.NUMBERS.DEGRADED.HARD_WRAP_UNITS", "title": "Repair hard-wrapped number and unit pairs", "source_refs": ["CMOS18 \u00a79.19 p599 (scan p621)"], "category": "numbers", "severity": "warn", "applies_to": "md", "rule_text": "When number and unit pairs are split by line breaks in source text, restore the pair and apply nonbreaking spacing if required.", "rationale": "Broken number-unit pairs reduce readability and can be misread.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect number-unit splits across line breaks and suggest recombining with a nonbreaking space.", "tags": ["degraded_input", "units"], "keywords": ["hard wraps", "units", "nonbreaking space"], "dependencies": [], "exceptions": ["Avoid changes in code blocks and data tables."], "status": "active"}

View file

@ -0,0 +1,15 @@
{"id":"CMOS.PUNCTUATION.COMMAS.SERIAL_COMMA.DEFAULT","title":"Use the serial comma by default","source_refs":["CMOS18 §6.19 p385 (scan p407)"],"category":"punctuation","severity":"should","applies_to":"md","rule_text":"In running text, include a comma before the final conjunction in a simple series (serial comma) unless a profile/house style overrides.","rationale":"The serial comma reduces ambiguity in lists.","enforcement":"lint","autofix":"suggest","autofix_notes":"Suggest adding a serial comma in simple three-item series when safe; do not rewrite complex lists automatically.","tags":["commas","serial_comma","lists"],"keywords":["serial comma","Oxford comma","list punctuation"],"dependencies":[],"exceptions":["Some house styles omit the serial comma; treat as profile override."],"status":"active"}
{"id":"CMOS.PUNCTUATION.COMMAS.INTRODUCTORY_ELEMENTS.CLARITY","title":"Use commas after introductory elements when needed for clarity","source_refs":["CMOS18 §6.26 p390 (scan p412)"],"category":"punctuation","severity":"should","applies_to":"all","rule_text":"After a substantial introductory phrase or clause, use a comma when it improves readability; omit the comma for very short, unambiguous introductions.","rationale":"Introductory commas prevent misreading and help scanning.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["commas","manual_checklist=true"],"keywords":["introductory comma","introductory clause"],"dependencies":[],"exceptions":["Headlines and telegraphic styles may omit commas; follow the selected profile."],"status":"active"}
{"id":"CMOS.PUNCTUATION.COMMAS.NONRESTRICTIVE.SET_OFF","title":"Set off nonrestrictive elements with commas","source_refs":["CMOS18 §6.17 p385 (scan p407)"],"category":"punctuation","severity":"should","applies_to":"all","rule_text":"Use commas to set off nonrestrictive (parenthetical) clauses and phrases that add information but are not essential to the sentences core meaning.","rationale":"Properly marking nonrestrictive elements improves clarity and reduces misinterpretation.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["commas","clauses","manual_checklist=true"],"keywords":["nonrestrictive clause","parenthetical"],"dependencies":[],"exceptions":["Some technical writing prefers tighter punctuation; document any deviation in house rules."],"status":"active"}
{"id":"CMOS.PUNCTUATION.COMMAS.RESTRICTIVE.NO_SET_OFF","title":"Do not set off restrictive elements with commas","source_refs":["CMOS18 §6.29 p392 (scan p414)"],"category":"punctuation","severity":"should","applies_to":"all","rule_text":"Do not use commas to set off restrictive clauses and phrases that are essential to identifying the noun they modify.","rationale":"Incorrectly comma-setting restrictive elements changes meaning.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["commas","clauses","manual_checklist=true"],"keywords":["restrictive clause","essential clause"],"dependencies":[],"exceptions":["If the distinction is ambiguous, prefer clarity and consider rewriting the sentence."],"status":"active"}
{"id":"CMOS.PUNCTUATION.SEMICOLONS.COMPLEX_SERIES.SEPARATE","title":"Use semicolons to separate complex list items","source_refs":["CMOS18 §6.64 p408 (scan p430)"],"category":"punctuation","severity":"should","applies_to":"all","rule_text":"In a series where individual items contain internal commas, use semicolons to separate the major items to avoid ambiguity.","rationale":"Semicolons clarify nested list structure.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["semicolons","lists","manual_checklist=true"],"keywords":["semicolon","complex series","list items"],"dependencies":[],"exceptions":["Bulleted lists may reduce the need for semicolons; apply judgment based on layout."],"status":"active"}
{"id":"CMOS.PUNCTUATION.DASHES.EM_DASH.USE_WITHOUT_SPACES_US","title":"Use em dashes without surrounding spaces (US style)","source_refs":["CMOS18 §6.89 p418 (scan p440)","CMOS18 §6.91 p418 (scan p440)"],"category":"punctuation","severity":"must","applies_to":"md","rule_text":"Use em dashes for parenthetical breaks in a sentence; in US style, set em dashes without surrounding spaces.","rationale":"Consistent dash styling improves readability and prevents mixed conventions.","enforcement":"lint","autofix":"rewrite","autofix_notes":"Normalize common em-dash patterns: convert spaced double-hyphen or spaced hyphen-hyphen to an em dash without spaces, excluding code blocks.","tags":["dashes","em_dash"],"keywords":["em dash","dash spacing","double hyphen"],"dependencies":[],"exceptions":["Some publishers use spaced em dashes; treat as profile override."],"status":"active"}
{"id":"CMOS.PUNCTUATION.DASHES.EN_DASH.RANGES","title":"Use en dashes for numeric ranges","source_refs":["CMOS18 §6.83 p415 (scan p437)"],"category":"punctuation","severity":"should","applies_to":"md","rule_text":"Use en dashes (not hyphens) to express simple numeric ranges (pages, dates, amounts) when the range is written in numerals.","rationale":"En dashes disambiguate ranges from hyphenation and minus signs.","enforcement":"lint","autofix":"rewrite","autofix_notes":"Rewrite digit-hyphen-digit patterns into digitdigit (en dash) when both sides are numerals and the hyphen is not a leading minus; skip code blocks.","tags":["dashes","en_dash","ranges"],"keywords":["en dash","range","page range","date range"],"dependencies":[],"exceptions":["Use from … to …’ or between … and …’ constructions rather than mixing with an en dash."],"status":"active"}
{"id":"CMOS.PUNCTUATION.HYPHENS.COMPOUND_MODIFIERS.BEFORE_NOUN","title":"Hyphenate compound modifiers before a noun when needed","source_refs":["CMOS18 §7.91 p476 (scan p498)"],"category":"punctuation","severity":"should","applies_to":"all","rule_text":"Hyphenate compound modifiers that precede a noun when the hyphen prevents ambiguity; do not automatically hyphenate compounds used after the noun.","rationale":"Hyphenation can prevent misreading of compound adjectives.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["hyphenation","compounds","manual_checklist=true"],"keywords":["compound modifier","hyphenation","ambiguity"],"dependencies":[],"exceptions":["Familiar open compounds and proper names may follow established usage; document house preferences."],"status":"active"}
{"id":"CMOS.PUNCTUATION.HYPHENS.ADVERB_LY.NO_HYPHEN","title":"Do not hyphenate -ly adverbs with following adjectives","source_refs":["CMOS18 §7.93 p476 (scan p498)"],"category":"punctuation","severity":"should","applies_to":"all","rule_text":"Do not hyphenate a compound consisting of an adverb ending in -ly and the adjective it modifies (e.g., highly detailed).","rationale":"-ly adverbs already mark the relationship and typically do not need hyphens.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["hyphenation","manual_checklist=true"],"keywords":["-ly","adverb","hyphenation"],"dependencies":[],"exceptions":["Some fixed expressions may be hyphenated in sources; preserve quotations verbatim."],"status":"active"}
{"id":"CMOS.PUNCTUATION.ELLIPSIS.FORMAT.CONSISTENT","title":"Format ellipses consistently","source_refs":["CMOS18 §12.59 p760 (scan p782)"],"category":"punctuation","severity":"should","applies_to":"md","rule_text":"Represent ellipses consistently across the document (either a single ellipsis character or three periods). When omitting text, ensure spacing and punctuation remain readable.","rationale":"Inconsistent ellipsis formatting looks like copy/paste noise and harms legibility.","enforcement":"lint","autofix":"suggest","autofix_notes":"Suggest normalizing runs of periods to the configured ellipsis style outside code blocks; avoid rewriting quotations.","tags":["ellipsis","consistency"],"keywords":["ellipsis","three dots","omission"],"dependencies":[],"exceptions":["Quoted material must preserve the sources punctuation unless editorial policy permits normalization with a note."],"status":"active"}
{"id":"CMOS.PUNCTUATION.QUOTATION_MARKS.DOUBLE_PRIMARY_US","title":"Use double quotation marks as the primary quotation marks (US style)","source_refs":["CMOS18 §6.122 p428 (scan p450)"],"category":"punctuation","severity":"should","applies_to":"all","rule_text":"In US style, use double quotation marks for primary quotations and single quotation marks for quotations within quotations.","rationale":"A consistent quotation hierarchy reduces ambiguity.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["quotation_marks","manual_checklist=true"],"keywords":["quotation marks","double quotes","single quotes","nested quotes"],"dependencies":[],"exceptions":["Code, identifiers, and literal strings may use single quotes by convention; do not rewrite code."],"status":"active"}
{"id":"CMOS.PUNCTUATION.QUOTATION_MARKS.PUNCTUATION_PLACEMENT_US","title":"Place commas and periods inside closing quotation marks (US style)","source_refs":["CMOS18 §6.122 p428 (scan p450)"],"category":"punctuation","severity":"should","applies_to":"all","rule_text":"In US style, place commas and periods inside closing quotation marks; place other punctuation (such as colons and semicolons) outside unless it belongs to the quoted matter.","rationale":"Consistent punctuation placement is a high-signal editorial correctness marker.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["quotation_marks","punctuation_placement","manual_checklist=true"],"keywords":["punctuation with quotes","commas in quotes","periods in quotes"],"dependencies":[],"exceptions":["Technical docs may quote code where punctuation is literal; preserve code exactly."],"status":"active"}
{"id":"CMOS.PUNCTUATION.BLOCK_QUOTES.NO_QUOTE_MARKS","title":"Omit quotation marks for block quotations","source_refs":["CMOS18 §12.31 p746 (scan p768)"],"category":"punctuation","severity":"must","applies_to":"md","rule_text":"When a quotation is formatted as a block quote, omit quotation marks and use the block formatting to signal quoted status.","rationale":"Block quotes should not carry redundant quotation marks; it clutters the page.","enforcement":"lint","autofix":"suggest","autofix_notes":"Suggest removing leading/trailing quote marks around Markdown blockquotes when the content is a single continuous quotation, excluding cases with nested quotes.","tags":["block_quotes","quotation_marks"],"keywords":["block quotation","blockquote","quotation marks"],"dependencies":[],"exceptions":["If a block quote contains multiple quoted fragments, retain internal quotation marks as needed."],"status":"active"}
{"id":"CMOS.PUNCTUATION.MULTIPLE_MARKS.AVOID_STACKING","title":"Avoid stacking multiple punctuation marks","source_refs":["CMOS18 §6.131 p432 (scan p454)"],"category":"punctuation","severity":"warn","applies_to":"md","rule_text":"Avoid stacking punctuation marks (e.g., ?!, !!, multiple consecutive question marks) in formal prose; prefer the single mark that conveys the intended meaning.","rationale":"Punctuation stacking reads as informal and weakens editorial tone.","enforcement":"lint","autofix":"suggest","autofix_notes":"Flag repeated punctuation runs outside code blocks and suggest a single appropriate mark.","tags":["punctuation_runs"],"keywords":["multiple punctuation","?!","!!"],"dependencies":[],"exceptions":["Direct quotations may preserve emphatic punctuation; keep quotes verbatim."],"status":"active"}
{"id":"CMOS.PUNCTUATION.PARENS_BRACKETS.BALANCE","title":"Ensure parentheses and brackets are balanced","source_refs":["CMOS18 §6.101 p422 (scan p444)","CMOS18 §6.103 p422 (scan p444)"],"category":"punctuation","severity":"must","applies_to":"md","rule_text":"Ensure parentheses and brackets are balanced and properly nested in running text.","rationale":"Unbalanced delimiters are a high-signal correctness failure.","enforcement":"lint","autofix":"none","autofix_notes":"","tags":["parentheses","brackets"],"keywords":["parentheses","brackets","balance"],"dependencies":[],"exceptions":["Code blocks and inline code may include unmatched delimiters as literals; exclude from lint."],"status":"active"}

View file

@ -0,0 +1,40 @@
{"id": "CMOS.PUNCTUATION.PERIODS.USE", "title": "Use periods to end declarative sentences", "source_refs": ["CMOS18 \u00a76.12 p383 (scan p405)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "End declarative sentences with a period in running prose; avoid dropping terminal punctuation in formal text.", "rationale": "Terminal punctuation prevents run-on reading.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "periods"], "keywords": ["periods", "terminal punctuation"], "dependencies": [], "exceptions": ["Display typography and headlines may omit periods by style."], "status": "active"}
{"id": "CMOS.PUNCTUATION.PERIODS.WITH_PARENS", "title": "Place periods correctly with parentheses", "source_refs": ["CMOS18 \u00a76.13 p383 (scan p405)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "When a parenthetical is a complete sentence, place the period inside the closing parenthesis; otherwise keep punctuation outside.", "rationale": "Placement clarifies whether the parenthetical stands alone.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "periods", "parentheses"], "keywords": ["parentheses", "period placement"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.COMPOUND_PREDICATES", "title": "Avoid commas between verbs in compound predicates", "source_refs": ["CMOS18 \u00a76.24 p388 (scan p410)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Do not insert a comma between verbs in a compound predicate unless needed for clarity.", "rationale": "Unnecessary commas interrupt sentence flow.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["compound predicate", "commas"], "dependencies": [], "exceptions": ["Insert a comma when needed to avoid misreading."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.DEPENDENT_CLAUSE_AFTER", "title": "Omit commas before dependent clauses that follow", "source_refs": ["CMOS18 \u00a76.27 p390 (scan p412)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "When a dependent clause follows the main clause, omit the comma unless needed for clarity.", "rationale": "Avoiding unnecessary commas keeps prose tight.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["dependent clause", "comma placement"], "dependencies": [], "exceptions": ["Insert a comma if it prevents ambiguity."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.APPOSITIVES", "title": "Use commas with appositives only when nonrestrictive", "source_refs": ["CMOS18 \u00a76.30 p392 (scan p414)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use commas to set off nonrestrictive appositives; avoid commas for restrictive appositives.", "rationale": "Comma placement changes meaning with appositives.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["appositives", "restrictive"], "dependencies": [], "exceptions": ["If the distinction is unclear, rewrite for clarity."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.DESCRIPTIVE_PHRASES", "title": "Set off nonessential descriptive phrases with commas", "source_refs": ["CMOS18 \u00a76.32 p394 (scan p416)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Set off descriptive phrases with commas when they are nonessential; omit commas when they are essential to identification.", "rationale": "Comma choice signals whether information is essential.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["descriptive phrases", "comma use"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.PARTICIPIAL_PHRASES", "title": "Use commas with participial phrases when nonrestrictive", "source_refs": ["CMOS18 \u00a76.33 p394 (scan p416)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use commas to set off participial phrases when they are introductory or nonrestrictive; omit when essential.", "rationale": "Participial phrases can be restrictive or parenthetical.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["participial phrases", "comma use"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.INTRODUCTORY_PHRASES", "title": "Use commas after substantial introductory phrases", "source_refs": ["CMOS18 \u00a76.36 p396 (scan p418)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use a comma after a substantial introductory phrase when it improves readability; omit for very short phrases.", "rationale": "Introductory commas help readers parse sentence structure.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["introductory phrases", "comma use"], "dependencies": [], "exceptions": ["Headlines and telegraphic styles may omit such commas."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.REPEATED_ADJECTIVES", "title": "Use commas between coordinate adjectives", "source_refs": ["CMOS18 \u00a76.40 p398 (scan p420)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Separate coordinate adjectives with commas when they independently modify the noun.", "rationale": "Commas clarify adjective relationships.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["coordinate adjectives", "comma use"], "dependencies": [], "exceptions": ["Do not separate cumulative adjectives with commas."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.DATES", "title": "Use commas in month-day-year dates in prose", "source_refs": ["CMOS18 \u00a76.41 p398 (scan p420)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use commas in month-day-year dates in running text; do not use them in day-month-year or all-numeral formats.", "rationale": "Comma placement signals date order.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas", "dates"], "keywords": ["dates", "comma placement"], "dependencies": [], "exceptions": ["Tables may use numeric formats without commas for alignment."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.ADDRESSES", "title": "Use commas to separate address elements in running text", "source_refs": ["CMOS18 \u00a76.42 p398 (scan p420)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use commas to separate elements in running-text addresses; avoid commas in postal address blocks.", "rationale": "Address punctuation differs between prose and postal formats.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["addresses", "commas"], "dependencies": [], "exceptions": ["Address blocks should follow postal standards without commas."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.QUOTED_TITLES", "title": "Place commas around quoted titles based on sentence structure", "source_refs": ["CMOS18 \u00a76.44 p400 (scan p422)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Place a comma after a quoted or italicized title only when the sentence requires it.", "rationale": "Punctuation should follow syntax, not decoration.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["quoted titles", "comma placement"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.COMMAS.QUESTIONS", "title": "Use commas with embedded questions only when required", "source_refs": ["CMOS18 \u00a76.45 p400 (scan p422)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use commas to set off a direct question embedded in a sentence only when required by structure.", "rationale": "Over-punctuation can obscure the question.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "commas"], "keywords": ["embedded questions", "comma use"], "dependencies": [], "exceptions": ["Rewrite if punctuation rules create awkward phrasing."], "status": "active"}
{"id": "CMOS.PUNCTUATION.SEMICOLONS.CONJUNCTIVE_PHRASES", "title": "Use semicolons before conjunctive phrases joining clauses", "source_refs": ["CMOS18 \u00a76.62 p408 (scan p430)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use a semicolon before conjunctive phrases like that is or for example when they join independent clauses.", "rationale": "Semicolons prevent comma splices in complex sentences.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "semicolons"], "keywords": ["semicolons", "conjunctive phrases"], "dependencies": [], "exceptions": ["A comma may be acceptable in short, informal sentences."], "status": "active"}
{"id": "CMOS.PUNCTUATION.SEMICOLONS.BEFORE_CONJUNCTION", "title": "Use semicolons before conjunctions in complex sentences", "source_refs": ["CMOS18 \u00a76.63 p408 (scan p430)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "A semicolon may precede a conjunction joining independent clauses when the separation needs emphasis or clarity.", "rationale": "Semicolons clarify long or complex clauses.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "semicolons"], "keywords": ["semicolons", "conjunctions"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.COLONS.SPACE_AFTER", "title": "Use a single space after a colon", "source_refs": ["CMOS18 \u00a76.66 p410 (scan p432)"], "category": "punctuation", "severity": "must", "applies_to": "md", "rule_text": "Use a single space after a colon in prose; avoid double spaces.", "rationale": "Extra spaces create inconsistent rhythm.", "enforcement": "lint", "autofix": "rewrite", "autofix_notes": "Normalize runs of spaces after a colon to a single space outside code blocks.", "tags": ["colons", "spacing"], "keywords": ["colon spacing", "single space"], "dependencies": [], "exceptions": ["Code and literal text may preserve spacing."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COLONS.CAPITALIZATION", "title": "Capitalize after a colon only when a full sentence follows", "source_refs": ["CMOS18 \u00a76.67 p410 (scan p432)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Capitalize the first word after a colon only when it introduces a complete sentence or a quotation per style.", "rationale": "Capitalization cues sentence structure.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "colons"], "keywords": ["colon capitalization", "sentence after colon"], "dependencies": [], "exceptions": ["Headlines or display text may follow different rules."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COLONS.AS_FOLLOWS", "title": "Use a colon after introductions like as follows", "source_refs": ["CMOS18 \u00a76.68 p410 (scan p432)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use a colon after introductions such as as follows or the following; avoid a colon after an incomplete clause.", "rationale": "Colons should follow a complete lead-in.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "colons"], "keywords": ["as follows", "colon usage"], "dependencies": [], "exceptions": ["In informal prose, a dash may replace a colon by choice."], "status": "active"}
{"id": "CMOS.PUNCTUATION.COLONS.INTRO_QUOTE_QUESTION", "title": "Use a colon to introduce quotations or questions when appropriate", "source_refs": ["CMOS18 \u00a76.69 p410 (scan p432)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "A colon may introduce a quotation or question when the lead-in is a complete clause.", "rationale": "Colons signal that what follows completes the lead-in.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "colons"], "keywords": ["quotation introduction", "colon usage"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.QUESTION_MARK.USE", "title": "Use question marks for direct questions", "source_refs": ["CMOS18 \u00a76.72 p412 (scan p434)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use a question mark for direct questions; do not add question marks to declarative statements.", "rationale": "Question marks mark interrogative sentences.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "question_marks"], "keywords": ["question marks", "direct questions"], "dependencies": [], "exceptions": ["Quoted material should preserve the source punctuation."], "status": "active"}
{"id": "CMOS.PUNCTUATION.QUESTION_MARK.DIRECT_VS_INDIRECT", "title": "Avoid question marks for indirect questions", "source_refs": ["CMOS18 \u00a76.73 p412 (scan p434)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Do not use a question mark for indirect questions unless the whole sentence is interrogative.", "rationale": "Indirect questions are statements.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "question_marks"], "keywords": ["indirect questions", "question marks"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.QUESTION_MARK.WITH_PUNCT", "title": "Do not stack question marks with other terminal punctuation", "source_refs": ["CMOS18 \u00a76.74 p413 (scan p435)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "When a question mark applies to the whole sentence, it replaces the period; avoid stacking terminal marks.", "rationale": "Stacked punctuation reads as informal and unclear.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "question_marks"], "keywords": ["stacked punctuation", "question marks"], "dependencies": [], "exceptions": ["Quoted material may preserve source punctuation."], "status": "active"}
{"id": "CMOS.PUNCTUATION.EXCLAMATION.USE_SPARINGLY", "title": "Use exclamation points sparingly in formal prose", "source_refs": ["CMOS18 \u00a76.75 p413 (scan p435)"], "category": "punctuation", "severity": "warn", "applies_to": "all", "rule_text": "Use exclamation points sparingly in formal prose; prefer declarative punctuation for emphasis.", "rationale": "Exclamation points can weaken a formal tone.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "exclamation_points"], "keywords": ["exclamation points", "tone"], "dependencies": [], "exceptions": ["Quoted dialogue may retain exclamation marks."], "status": "active"}
{"id": "CMOS.PUNCTUATION.EXCLAMATION.VS_QUESTION", "title": "Use question marks for questions even when emphatic", "source_refs": ["CMOS18 \u00a76.76 p413 (scan p435)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use a question mark for interrogative sentences even if the tone is forceful; avoid substituting exclamation points.", "rationale": "Question marks signal grammatical form, not tone.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "exclamation_points", "question_marks"], "keywords": ["questions", "exclamation points"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.DASHES.EM_LINE_BREAKS", "title": "Avoid line breaks adjacent to em dashes", "source_refs": ["CMOS18 \u00a76.96 p420 (scan p442)"], "category": "punctuation", "severity": "should", "applies_to": "pdf", "rule_text": "Avoid line breaks immediately before or after an em dash in justified text; reflow if needed.", "rationale": "Em-dash line breaks create awkward spacing.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Adjust line breaking or tracking to keep em dashes off line boundaries; re-render to confirm.", "tags": ["dashes", "line_breaks"], "keywords": ["em dash", "line breaks"], "dependencies": [], "exceptions": ["Very narrow columns may require a break; record as QA warning."], "status": "active"}
{"id": "CMOS.PUNCTUATION.DASHES.EM_INSTEAD_OF_QUOTES", "title": "Avoid using em dashes as quotation marks in formal prose", "source_refs": ["CMOS18 \u00a76.97 p420 (scan p442)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Do not substitute em dashes for quotation marks in formal prose unless the style explicitly calls for it.", "rationale": "Em-dash dialogue conventions are not universal.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "dashes"], "keywords": ["em dash", "quotation marks"], "dependencies": [], "exceptions": ["Fiction styles that use em-dash dialogue are exempt."], "status": "active"}
{"id": "CMOS.PUNCTUATION.PARENS.USE", "title": "Use parentheses for incidental material", "source_refs": ["CMOS18 \u00a76.101 p422 (scan p444)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use parentheses for incidental, nonessential material; do not use them where commas or dashes would be clearer.", "rationale": "Overuse of parentheses interrupts reading flow.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "parentheses"], "keywords": ["parentheses", "incidental material"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.PARENS.GLOSSES_TRANSLATIONS", "title": "Use parentheses for brief glosses or translations", "source_refs": ["CMOS18 \u00a76.102 p422 (scan p444)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use parentheses to provide brief glosses or translations in running text.", "rationale": "Parentheses signal supplementary information.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "parentheses"], "keywords": ["glosses", "translations"], "dependencies": [], "exceptions": ["Footnotes may be preferable for long explanations."], "status": "active"}
{"id": "CMOS.PUNCTUATION.PARENS.NESTING", "title": "Use brackets for parentheses within parentheses", "source_refs": ["CMOS18 \u00a76.103 p422 (scan p444)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "When parentheses are nested, use a different enclosure (such as brackets) for the inner level.", "rationale": "Distinct enclosures reduce nesting confusion.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "parentheses", "brackets"], "keywords": ["nested parentheses", "brackets"], "dependencies": [], "exceptions": ["Math notation may use other bracket conventions."], "status": "active"}
{"id": "CMOS.PUNCTUATION.BRACKETS.TRANSLATED_TEXT", "title": "Use square brackets for editorial insertions in translations", "source_refs": ["CMOS18 \u00a76.106 p424 (scan p446)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use square brackets to mark editorial insertions or clarifications in translated text.", "rationale": "Brackets distinguish editor additions from the translation.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "brackets"], "keywords": ["translated text", "editorial insertions"], "dependencies": [], "exceptions": [], "status": "active"}
{"id": "CMOS.PUNCTUATION.BRACKETS.NESTED_PARENS", "title": "Use brackets for parentheses within parentheses", "source_refs": ["CMOS18 \u00a76.107 p424 (scan p446)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use square brackets when you need parentheses inside parentheses to avoid confusion.", "rationale": "Distinct enclosures clarify structure.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "brackets"], "keywords": ["nested punctuation", "brackets"], "dependencies": [], "exceptions": ["Math notation may use braces or other conventions."], "status": "active"}
{"id": "CMOS.PUNCTUATION.SLASHES.ALTERNATIVES", "title": "Use slashes only for true alternatives", "source_refs": ["CMOS18 \u00a76.113 p426 (scan p448)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use slashes for true alternatives only; prefer words when clarity is at risk.", "rationale": "Slashes can be ambiguous without context.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "slashes"], "keywords": ["slashes", "alternatives"], "dependencies": [], "exceptions": ["Technical abbreviations may use slashes by convention."], "status": "active"}
{"id": "CMOS.PUNCTUATION.SLASHES.TWO_YEAR_SPANS", "title": "Use slashes for two-year spans only when the style allows", "source_refs": ["CMOS18 \u00a76.114 p426 (scan p448)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use slashes for two-year spans only when the style allows; otherwise use an en dash or words.", "rationale": "Consistent range notation avoids confusion.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "slashes"], "keywords": ["two-year spans", "slashes"], "dependencies": [], "exceptions": ["Budget years may follow institutional slash styles."], "status": "active"}
{"id": "CMOS.PUNCTUATION.QUOTES.SMART_QUOTES", "title": "Use typographic quotation marks in published output", "source_refs": ["CMOS18 \u00a76.123 p428 (scan p450)"], "category": "punctuation", "severity": "must", "applies_to": "md", "rule_text": "Use typographic (smart) quotation marks in published output; avoid straight quotes outside code.", "rationale": "Smart quotes improve typographic quality and readability.", "enforcement": "lint", "autofix": "rewrite", "autofix_notes": "Convert straight quotes to typographic quotes in prose; skip code blocks and inline code.", "tags": ["quotation_marks", "degraded_input"], "keywords": ["smart quotes", "quotation marks"], "dependencies": [], "exceptions": ["Literal strings and code must preserve straight quotes."], "status": "active"}
{"id": "CMOS.PUNCTUATION.HYPHENATION.GENERAL_CHOICE", "title": "Hyphenate only when it improves clarity", "source_refs": ["CMOS18 \u00a77.87 p474 (scan p496)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Hyphenate compounds only when doing so improves clarity; avoid unnecessary hyphens in familiar closed compounds.", "rationale": "Unnecessary hyphens add visual noise.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "hyphenation"], "keywords": ["hyphenation", "compounds"], "dependencies": [], "exceptions": ["Follow dictionary or house lists for specific compounds."], "status": "active"}
{"id": "CMOS.PUNCTUATION.HYPHENATION.COMPOUND_DEFINITION", "title": "Differentiate open, hyphenated, and closed compounds", "source_refs": ["CMOS18 \u00a77.88 p475 (scan p497)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Treat open, hyphenated, and closed compounds distinctly and follow dictionary or house usage for each.", "rationale": "Compound form affects readability and meaning.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "hyphenation"], "keywords": ["open compounds", "closed compounds"], "dependencies": [], "exceptions": ["Technical terms may follow domain-specific conventions."], "status": "active"}
{"id": "CMOS.PUNCTUATION.HYPHENATION.TREND_CLOSED", "title": "Prefer closed forms for well-established compounds", "source_refs": ["CMOS18 \u00a77.89 p475 (scan p497)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Prefer closing compounds that are well established; avoid hyphens that the dictionary no longer supports.", "rationale": "Closed forms reflect common usage and reduce clutter.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "hyphenation"], "keywords": ["closed compounds", "dictionary usage"], "dependencies": [], "exceptions": ["Hyphenate when necessary for clarity or according to house style."], "status": "active"}
{"id": "CMOS.PUNCTUATION.HYPHENATION.READABILITY", "title": "Use hyphens to prevent misreading", "source_refs": ["CMOS18 \u00a77.90 p475 (scan p497)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use a hyphen when it prevents misreading in compound modifiers or ambiguous phrases.", "rationale": "Hyphens can disambiguate compound meaning.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "hyphenation"], "keywords": ["hyphenation", "clarity"], "dependencies": [], "exceptions": ["Do not hyphenate familiar open compounds that are unambiguous."], "status": "active"}
{"id": "CMOS.PUNCTUATION.HYPHENATION.SUSPENDED", "title": "Use suspended hyphens only in clear parallel structures", "source_refs": ["CMOS18 \u00a77.95 p477 (scan p499)"], "category": "punctuation", "severity": "should", "applies_to": "all", "rule_text": "Use suspended hyphens only when the shared element is clear and the parallel structure is short.", "rationale": "Suspended hyphens can confuse readers if overused.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "hyphenation"], "keywords": ["suspended hyphens", "parallel structure"], "dependencies": [], "exceptions": ["If clarity suffers, repeat the full compound instead."], "status": "active"}
{"id": "CMOS.PUNCTUATION.DEGRADED.EXTRA_SPACES.AFTER_PUNCT", "title": "Normalize extra spaces after punctuation in degraded text", "source_refs": ["CMOS18 \u00a76.66 p410 (scan p432)"], "category": "punctuation", "severity": "warn", "applies_to": "md", "rule_text": "Normalize multiple spaces after punctuation that arise from OCR or hard wraps; keep a single space in prose.", "rationale": "Extra spaces break text rhythm and complicate reflow.", "enforcement": "lint", "autofix": "rewrite", "autofix_notes": "Collapse runs of spaces after punctuation to a single space outside code blocks.", "tags": ["degraded_input", "spacing"], "keywords": ["OCR", "extra spaces", "punctuation"], "dependencies": [], "exceptions": ["Preserve spacing in code blocks and preformatted text."], "status": "active"}

View file

@ -0,0 +1,8 @@
{"id":"BRING.TABLES.EDIT_AS_TEXT.READABILITY","title":"Edit tables as carefully as prose and set them to be read","source_refs":["BRING §4.4.1 p70 (scan p69)"],"category":"tables","severity":"should","applies_to":"all","rule_text":"Treat tables as text that must be readable; editorial cleanup and structure clarity matter as much as typographic alignment.","rationale":"Unreadable tables are usually an editorial problem first.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true"],"keywords":["tables","readability","editorial"],"dependencies":[],"exceptions":["For purely decorative tables, consider converting to a figure instead of forcing tabular typesetting."],"status":"active"}
{"id":"BRING.TABLES.FURNITURE.MINIMIZE","title":"Minimize table furniture and maximize information","source_refs":["BRING §4.4.1 p70 (scan p69)"],"category":"tables","severity":"should","applies_to":"all","rule_text":"Use the minimum necessary rules/boxes/dots; prefer whitespace and alignment for structure whenever possible.","rationale":"Excess rules add noise and reduce clarity.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Flag heavy grid styles and suggest reducing borders in favor of spacing and alignment.","tags":["tables","furniture"],"keywords":["table rules","gridlines","whitespace","alignment"],"dependencies":[],"exceptions":["Dense financial tables may require light rules; keep them subtle and consistent."],"status":"active"}
{"id":"BRING.TABLES.TEXT_ORIENTATION.HORIZONTAL","title":"Keep table text horizontal (avoid vertical column headers)","source_refs":["BRING §4.4.1 p70 (scan p69)"],"category":"tables","severity":"should","applies_to":"all","rule_text":"Avoid rotating column headers as a space-saving trick in Latin-alphabet documents; redesign the table instead of forcing vertical reading.","rationale":"Vertical headers are slower to read and easier to misinterpret.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true"],"keywords":["column headers","rotation","tables"],"dependencies":[],"exceptions":["Non-Latin scripts with vertical reading conventions may allow different orientation choices."],"status":"active"}
{"id":"BRING.TABLES.TYPE_SIZE.READABLE","title":"Do not solve table overflow by making type illegible","source_refs":["BRING §4.4.1 p70 (scan p69)"],"category":"tables","severity":"should","applies_to":"pdf","rule_text":"Avoid shrinking or condensing table type below comfortable readability; prefer reflow, wrapping, or rethinking the table structure.","rationale":"Legibility is more important than packing density.","enforcement":"typeset","autofix":"suggest","autofix_notes":"If table scaling exceeds the profile shrink limit, suggest wrapping or splitting the table.","tags":["tables","overflow"],"keywords":["table overflow","shrink limit","legibility"],"dependencies":[],"exceptions":["Very large appendices may use smaller type if the profile explicitly permits it."],"status":"active"}
{"id":"BRING.TABLES.GUIDES.READING_DIRECTION","title":"Guides and dividers should follow the reading direction","source_refs":["BRING §4.4.1 p70 (scan p69)"],"category":"tables","severity":"should","applies_to":"all","rule_text":"When guides (rules, tint blocks) are necessary, align them with the dominant reading direction (horizontal for most tables; vertical for certain lists/indices).","rationale":"Directional cues should match how readers scan the data.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true"],"keywords":["table guides","rules","reading direction","lists"],"dependencies":[],"exceptions":["Heatmap-style tables may use tint blocks if they improve data comprehension."],"status":"active"}
{"id":"HOUSE.TABLES.OVERFLOW.NO_CLIPPING","title":"Tables must not clip off-page; overflow must be handled explicitly","source_refs":["HOUSE §QA.TABLE_OVERFLOW p2"],"category":"tables","severity":"must","applies_to":"pdf","rule_text":"If a table exceeds the available measure, the renderer must wrap/shrink/split according to profile policy; clipping is never acceptable.","rationale":"Clipped tables silently destroy data.","enforcement":"postrender","autofix":"reflow","autofix_notes":"When a table overflow incident is detected, re-render using a stricter overflow strategy (wrap → bounded shrink → split).","tags":["overflow","tables"],"keywords":["table overflow","clipping","reflow"],"dependencies":[],"exceptions":["If a table cannot be reflowed without altering meaning, emit a clear overflow warning and require manual intervention."],"status":"active"}
{"id":"HOUSE.TABLES.HEADERS.REPEAT_ON_PAGEBREAK","title":"Repeat table header rows when tables break across pages","source_refs":["HOUSE §QA.TABLE_OVERFLOW p2"],"category":"tables","severity":"should","applies_to":"pdf","rule_text":"When a table spans multiple pages, repeat the header row to preserve meaning on each page.","rationale":"Readers should not need to flip back to decode columns.","enforcement":"typeset","autofix":"suggest","autofix_notes":"If the renderer supports repeated headers, enable it for multi-page tables; otherwise warn.","tags":["tables","header_repeat"],"keywords":["repeat headers","multi-page tables"],"dependencies":[],"exceptions":["For very small tables, header repetition is unnecessary."],"status":"active"}
{"id":"HOUSE.TABLES.ALIGNMENT.DECIMALS","title":"Align numeric columns by decimal when possible","source_refs":["HOUSE §QA.TABLE_OVERFLOW p2"],"category":"tables","severity":"should","applies_to":"all","rule_text":"For numeric columns, align values by decimal point (or consistent digit grouping) to make magnitude comparisons easier.","rationale":"Numeric alignment improves scanability.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Suggest decimal-aligned column styles for detected numeric columns.","tags":["tables","numbers"],"keywords":["decimal alignment","numeric columns","scanability"],"dependencies":[],"exceptions":["Mixed units or textual annotations may require ragged-right alignment."],"status":"active"}

View file

@ -0,0 +1,15 @@
{"id": "BRING.TABLES.TITLES.CONSISTENT_PLACEMENT", "title": "Place table titles consistently", "source_refs": ["BRING \u00a74.4 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Place table titles or labels consistently (above or below) and follow the same pattern throughout the document.", "rationale": "Consistent placement aids scanning.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables"], "keywords": ["table titles", "placement"], "dependencies": [], "exceptions": ["If a style guide mandates placement, follow it consistently."], "status": "active"}
{"id": "BRING.TABLES.CAPTIONS.CLEAR", "title": "Write clear, descriptive table captions", "source_refs": ["BRING \u00a74.4 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Write table captions that state what the table shows, not just the topic.", "rationale": "Clear captions help readers interpret data.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables"], "keywords": ["table captions", "clarity"], "dependencies": [], "exceptions": ["Very small tables may rely on surrounding text for context."], "status": "active"}
{"id": "BRING.TABLES.HEADERS.CONCISE", "title": "Keep column headers concise", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Keep column headers concise and readable; avoid unnecessary words.", "rationale": "Concise headers reduce visual clutter.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables"], "keywords": ["column headers", "concise"], "dependencies": [], "exceptions": ["Regulatory tables may require full legal labels."], "status": "active"}
{"id": "BRING.TABLES.HEADERS.ALIGN_WITH_COLUMNS", "title": "Align headers with their data columns", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "pdf", "rule_text": "Align column headers with their data columns and avoid misalignment caused by decorative spacing.", "rationale": "Alignment helps readers map labels to data.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Adjust column widths and header alignment to match data columns.", "tags": ["tables", "typeset"], "keywords": ["header alignment", "columns"], "dependencies": [], "exceptions": ["Complex multi-level headers may need manual layout."], "status": "active"}
{"id": "BRING.TABLES.UNITS.IN_HEADERS", "title": "Put units in headers when possible", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Put units of measure in headers or stub labels rather than repeating them in every cell.", "rationale": "Shared units reduce repetition and improve readability.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables", "units"], "keywords": ["units", "headers"], "dependencies": [], "exceptions": ["If units vary by row, include them explicitly in the data."], "status": "active"}
{"id": "BRING.TABLES.STUB_COLUMN.USE", "title": "Use a stub column for row labels when needed", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Use a stub (row-header) column when it improves row identification.", "rationale": "Stub columns anchor row meaning.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables"], "keywords": ["stub column", "row labels"], "dependencies": [], "exceptions": ["Very small tables may omit stubs if context is clear."], "status": "active"}
{"id": "BRING.TABLES.GROUPING.WHITESPACE", "title": "Use whitespace to group rows and columns", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "pdf", "rule_text": "Use whitespace and alignment to group rows and columns; avoid heavy rules when spacing will do.", "rationale": "Whitespace groups data without adding visual noise.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Reduce heavy rules and increase row or column spacing where possible.", "tags": ["tables", "typeset"], "keywords": ["whitespace", "grouping"], "dependencies": [], "exceptions": ["Dense financial tables may require light rules for clarity."], "status": "active"}
{"id": "BRING.TABLES.ROW_SPACING.READABLE", "title": "Keep row spacing readable", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "pdf", "rule_text": "Maintain adequate row spacing for readability; avoid crowding rows to fit width.", "rationale": "Crowded rows make tables hard to scan.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Increase row spacing or split the table when rows are crowded.", "tags": ["tables", "typeset"], "keywords": ["row spacing", "readability"], "dependencies": [], "exceptions": ["Very compact tables may accept tighter spacing if legibility remains."], "status": "active"}
{"id": "BRING.TABLES.COLUMN_ALIGNMENT.CONSISTENT", "title": "Align data consistently by type", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "pdf", "rule_text": "Align data consistently by type (text left, numerals right or decimal-aligned).", "rationale": "Consistent alignment improves scanability.", "enforcement": "typeset", "autofix": "suggest", "autofix_notes": "Apply column alignment rules based on detected data types.", "tags": ["tables", "typeset"], "keywords": ["alignment", "numeric columns"], "dependencies": [], "exceptions": ["Mixed-content columns may require manual alignment decisions."], "status": "active"}
{"id": "BRING.TABLES.SOURCES.NOTES.DISTINCT", "title": "Separate table notes and sources from data", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Separate table notes and source lines from the data body with spacing or a clear separator.", "rationale": "Distinct notes prevent misreading data as footnotes.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables"], "keywords": ["table notes", "sources"], "dependencies": [], "exceptions": ["Very small tables may embed notes inline if clearly labeled."], "status": "active"}
{"id": "BRING.TABLES.MULTI_LINE_HEADERS.AVOID", "title": "Avoid stacked multi-line headers when possible", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Avoid multi-line stacked headers when possible; consider splitting or redesigning the table.", "rationale": "Stacked headers are slower to interpret.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables"], "keywords": ["multi-line headers", "table redesign"], "dependencies": [], "exceptions": ["If stacking is required, ensure clear alignment and spacing."], "status": "active"}
{"id": "BRING.TABLES.NUMERIC_PRECISION.CONSISTENT", "title": "Keep numeric precision consistent within a column", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Use consistent numeric precision within a column unless variation is meaningful.", "rationale": "Consistent precision makes comparisons easier.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables", "numbers"], "keywords": ["precision", "numeric columns"], "dependencies": [], "exceptions": ["Scientific tables may vary precision intentionally; document the rule."], "status": "active"}
{"id": "BRING.TABLES.DATA_TYPES.NOT_MIXED", "title": "Avoid mixing text and numerals within a column", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Avoid mixing text and numerals within a single data column unless labeled clearly.", "rationale": "Mixed types reduce scanability and alignment.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables"], "keywords": ["data types", "column consistency"], "dependencies": [], "exceptions": ["Annotated values may be necessary; use separate columns if possible."], "status": "active"}
{"id": "BRING.TABLES.ORDER.LOGICAL", "title": "Order rows and columns logically", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "should", "applies_to": "all", "rule_text": "Order rows and columns logically (alphabetical, chronological, or by magnitude) and document the choice.", "rationale": "Logical ordering helps readers find information.", "enforcement": "manual", "autofix": "none", "autofix_notes": "", "tags": ["manual_checklist=true", "tables"], "keywords": ["ordering", "row order"], "dependencies": [], "exceptions": ["Original source order may be retained if it carries meaning."], "status": "active"}
{"id": "BRING.TABLES.DEGRADED.REBUILD_COLUMNS", "title": "Rebuild column structure in degraded tables", "source_refs": ["BRING \u00a74.4.1 p70 (scan p69)"], "category": "tables", "severity": "warn", "applies_to": "md", "rule_text": "When OCR or imports collapse columns, reconstruct the column structure and flag uncertain alignments.", "rationale": "Degraded tables can misplace data values.", "enforcement": "lint", "autofix": "suggest", "autofix_notes": "Detect column-like spacing and suggest table reconstruction; flag low-confidence cells.", "tags": ["degraded_input", "tables"], "keywords": ["OCR", "table reconstruction"], "dependencies": [], "exceptions": ["Avoid auto-reconstruction for complex multi-level headers without review."], "status": "active"}

View file

@ -0,0 +1,8 @@
{"id":"BRING.TYPOGRAPHY.SPACING.WORD_SPACE.DEFINE","title":"Define the word space relative to type size","source_refs":["BRING §2.1.1 p25"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Treat interword spacing as a first-class typographic parameter: aim for a consistent text texture appropriate to the face and size, and avoid extremes that create visible rivers or cramped color.","rationale":"Stable word spacing underpins readability and prevents distracting texture artifacts.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Suggest adjusting justification and word-spacing settings; requires human confirmation in rendered output.","tags":["word_spacing","justification","text_texture"],"keywords":["word space","word spacing","justification","rivers","text color"],"dependencies":[],"exceptions":["Display typography may intentionally vary spacing for effect."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.SPACING.SENTENCE_SPACE.SINGLE","title":"Use a single word space between sentences","source_refs":["BRING §2.1.4 p27"],"category":"typography","severity":"must","applies_to":"md","rule_text":"In body text, use one interword space between sentences; do not insert double spaces after terminal punctuation.","rationale":"Extra sentence spacing creates uneven rhythm and can break reflowed layouts.","enforcement":"lint","autofix":"rewrite","autofix_notes":"Normalize runs of 2+ spaces after sentence-ending punctuation to a single space, excluding code blocks and inline code.","tags":["sentence_spacing"],"keywords":["single space","double space","sentence spacing"],"dependencies":[],"exceptions":["Fixed-width/plaintext artifacts (e.g., ASCII tables) may require preservation."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.HYPHENATION.LANGUAGE_DICTIONARY.MATCH","title":"Hyphenation must follow the document language","source_refs":["BRING §2-4.6 p43"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Hyphenation decisions should follow the conventions of the texts language; ensure the hyphenation dictionary matches the declared language(s).","rationale":"Wrong-language hyphenation yields incorrect breaks and reduces trust in the typeset output.","enforcement":"typeset","autofix":"suggest","autofix_notes":"If language metadata is missing or mismatched, recommend setting language explicitly for the document and sections before rendering.","tags":["hyphenation","i18n"],"keywords":["hyphenation","dictionary","language"],"dependencies":[],"exceptions":["Mixed-language documents may need per-section language tagging."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.HYPHENATION.AVOID_PROPER_NAMES","title":"Avoid hyphenating proper names when possible","source_refs":["BRING §2-4.5 p43"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Prefer not to hyphenate proper names; treat name breaks as a last resort and favor reflow or tracking adjustments first.","rationale":"Name breaks are visually jarring and can read as errors.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","hyphenation","proper_names"],"keywords":["proper names","hyphenation"],"dependencies":[],"exceptions":["Narrow measures may force a compromise; document the choice in QA notes."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.HYPHENATION.HARD_SPACES.SHORT_EXPRESSIONS","title":"Use hard spaces for short numeric expressions where appropriate","source_refs":["BRING §2-4.7 p43"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Prevent awkward line breaks in short numeric expressions (e.g., number + unit) by using non-breaking spacing where it improves clarity.","rationale":"Keeping tightly bound expressions together reduces misreading and layout noise.","enforcement":"lint","autofix":"suggest","autofix_notes":"Suggest inserting non-breaking spaces between common number-unit patterns when safe; never change inside code blocks.","tags":["non_breaking_space","numbers_units"],"keywords":["non-breaking space","number unit","line break"],"dependencies":[],"exceptions":["Do not force non-breaking behavior that causes overflow in narrow layouts."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.HYPHENATION.MIN_LEFT_RIGHT","title":"Set minimum characters around hyphen breaks","source_refs":["BRING §2-4.2 p42"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Configure hyphenation so that very short fragments are not left at either side of a hyphen break; require a minimum run of characters before and after the break.","rationale":"Tiny fragments look accidental and impair scanning.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Set hyphenation minimum-left and minimum-right parameters in the renderer profile.","tags":["hyphenation"],"keywords":["hyphenation","minimum left","minimum right"],"dependencies":[],"exceptions":["Languages with different hyphenation conventions may require different minima."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.HYPHENATION.MAX_CONSECUTIVE_LINES","title":"Limit consecutive hyphenated lines","source_refs":["BRING §2-4.4 p43"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Limit the number of consecutive hyphenated lines to reduce visual noise; prefer reflow or alternative breaks when a run of hyphenations would occur.","rationale":"Long runs of hyphens create a ragged texture that distracts from content.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Set a maximum consecutive hyphenated lines value in the renderer profile; requires hyphenation-aware layout engine.","tags":["hyphenation"],"keywords":["hyphenation","consecutive lines"],"dependencies":[],"exceptions":["Very narrow measures may require relaxing this rule; record as a QA warning."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.HYPHENATION.AVOID_AFTER_SHORT_LINE","title":"Avoid hyphenation following very short lines","source_refs":["BRING §2-4.8 p43"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Avoid hyphenating a word immediately after an unusually short line when it creates a pattern of broken rhythm; prefer adjusting line breaks instead.","rationale":"This pattern draws the eye to the margin and away from the text.","enforcement":"postrender","autofix":"none","autofix_notes":"","tags":["hyphenation","rhythm"],"keywords":["hyphenation","short line","rhythm"],"dependencies":[],"exceptions":["May be acceptable in very tight columns if overflow would otherwise occur."],"status":"active"}

View file

@ -0,0 +1,7 @@
{"id":"BRING.TYPOGRAPHY.SPACING.INITIALS.THIN_SPACE","title":"Use thin or no spaces in strings of initials","source_refs":["BRING §2.1.5 p30 (scan p29)"],"category":"typography","severity":"should","applies_to":"all","rule_text":"In sequences of personal initials separated by periods, avoid full word spaces between the initials; use very narrow spacing (or none) between the initial+period groups, then follow the final period with a normal word space before the surname.","rationale":"Full word spaces between initials create distracting gaps and uneven rhythm in running text.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","initials","spacing"],"keywords":["initials","W.B.","J.C.L.","hair space","thin space"],"dependencies":[],"exceptions":["If the output medium cannot represent thin spacing, prefer no extra space rather than a full word space."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.TRACKING.CAPS_SMALLCAPS_AND_LONG_DIGITS","title":"Add moderate tracking to caps/small caps and long digit strings","source_refs":["BRING §2.1.6 p30 (scan p29)"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Apply modest tracking to runs of ALL CAPS and Small Caps, and to long strings of digits, to improve legibility; keep the tracking subtle and verify that word and number recognition remain easy.","rationale":"Caps and long digit strings often need extra spacing to read cleanly at text sizes.","enforcement":"typeset","autofix":"suggest","autofix_notes":"When text is styled as all-caps or small caps, apply a conservative letter-spacing token; for long digit runs, allow a smaller tracking token. Validate in rendered output and avoid pushing words apart.","tags":["tracking","caps","small_caps","digits"],"keywords":["letterspacing","tracking","caps","small caps","digits"],"dependencies":[],"exceptions":["Do not apply this globally to mixed-case body text; restrict to explicit caps/small-caps styles and clearly identified digit runs."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.TRACKING.LOWERCASE.AVOID","title":"Avoid letterspacing lowercase without reason","source_refs":["BRING §2.1.7 p31 (scan p30)"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Do not apply letterspacing to lowercase body text by default. If tracking is used in lowercase for a specific design purpose, keep it minimal and review legibility.","rationale":"Lowercase letterspacing reduces legibility by disrupting word shapes and internal counters.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Ensure default body typography does not add letter-spacing to lowercase runs; if a display style applies tracking, confine it to short phrases and verify visually.","tags":["tracking","lowercase"],"keywords":["lowercase","letterspacing","tracking","legibility"],"dependencies":[],"exceptions":["Some display treatments may use tracking for effect; document the exception and verify readability."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.KERNING.CONSISTENT_OR_NONE","title":"Kern consistently and modestly (or not at all)","source_refs":["BRING §2.1.8 p32-33"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Use kerning consistently and modestly; inconsistent kerning is worse than none. Prefer font-provided kerning tables, and if automated kerning is applied, test and adjust only where it clearly improves consistency.","rationale":"Kerning errors and inconsistency stand out immediately and degrade trust in the typeset output.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","kerning"],"keywords":["kerning","kerning tables","pairs","consistency"],"dependencies":[],"exceptions":["Very small sizes and low-resolution output may limit perceived kerning benefits; prioritize consistency over micro-optimization."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.LETTERFORMS.AVOID_DISTORTION","title":"Dont distort letterforms (avoid arbitrary width/shape edits)","source_refs":["BRING §2.1.9 p35 (scan p34)"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Do not arbitrarily condense, expand, or otherwise distort letterforms to make text fit. Prefer choosing a suitable face/measure, reflowing content, or using a designed condensed/expanded family instead of scaling letter shapes.","rationale":"Distorting letterforms harms readability and makes documents look unprofessional.","enforcement":"typeset","autofix":"suggest","autofix_notes":"Avoid applying horizontal/vertical scaling to body fonts in CSS/typeset tokens. Prefer wrap/reflow and conservative overflow policies before any shrink/scale adjustments.","tags":["letterforms","distortion","scaling"],"keywords":["condense","expand","distort","scaleX","font stretch"],"dependencies":[],"exceptions":["If a constrained medium forces a compromise, keep scaling minimal and record as a QA note for sign-off."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.ALIGNMENT.DONT_STRETCH_SPACE","title":"Dont stretch spacing to force alignment (use leaders/tables instead)","source_refs":["BRING §2.1.10 p35 (scan p34)"],"category":"typography","severity":"should","applies_to":"md","rule_text":"Avoid using stretched or repeated spaces to align text in running copy (including table-of-contents and list-like layouts). Use proper tables, tab stops, or leader mechanisms instead of forcing alignment with whitespace.","rationale":"Whitespace-for-alignment breaks reflow, creates uneven texture, and fails across devices and renderers.","enforcement":"lint","autofix":"suggest","autofix_notes":"Detect lines with alignment-by-spacing patterns (e.g., long runs of spaces) outside code blocks and suggest converting to a table or a structured list with explicit layout.","tags":["alignment","spacing","leaders"],"keywords":["dot leaders","tab leaders","alignment","tables","spacing"],"dependencies":[],"exceptions":["Preformatted/code blocks and ASCII tables should not be altered."],"status":"active"}
{"id":"BRING.TYPOGRAPHY.NUMBER_STRINGS.SPACE_FOR_READABILITY","title":"Consider spacing for long strings of numbers","source_refs":["BRING §2.1.6 p30 (scan p29)"],"category":"typography","severity":"should","applies_to":"all","rule_text":"Where long numeric strings (serial numbers, phone numbers, IDs) appear in running text, ensure they remain readable by avoiding cramped setting; use appropriate grouping or subtle spacing consistent with the documents locale and typography.","rationale":"Long number strings can become illegible when set too tightly, especially at small sizes.","enforcement":"manual","autofix":"none","autofix_notes":"","tags":["manual_checklist=true","numbers","readability"],"keywords":["numbers","serial number","phone number","grouping"],"dependencies":[],"exceptions":["Do not change the semantic content of identifiers; only adjust spacing/grouping when it is already permitted by the source representation."],"status":"active"}

View file

@ -0,0 +1,181 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.invalid/iftypeset/spec/schema/rule.schema.json",
"title": "Publication-quality rule record",
"type": "object",
"additionalProperties": false,
"required": [
"id",
"title",
"source_refs",
"category",
"severity",
"applies_to",
"rule_text",
"rationale",
"enforcement",
"autofix",
"autofix_notes",
"tags",
"keywords",
"dependencies",
"exceptions",
"status"
],
"properties": {
"id": {
"type": "string",
"description": "Stable rule identifier. Prefix must be one of CMOS, BRING, HOUSE.",
"minLength": 6,
"maxLength": 120,
"pattern": "^(CMOS|BRING|HOUSE)\\.[A-Z0-9_]+(?:\\.[A-Z0-9_]+)*$"
},
"title": {
"type": "string",
"description": "Short human-readable rule title.",
"minLength": 4,
"maxLength": 160
},
"source_refs": {
"type": "array",
"description": "Pointers back to sources (not quotes). Prefer: CMOS18 §X.Y pN / BRING §X.Y pN / HOUSE §X.Y pN.",
"minItems": 1,
"items": {
"type": "string",
"minLength": 8,
"maxLength": 120,
"pattern": "^(CMOS18|BRING|HOUSE)\\s§[0-9A-Za-z][0-9A-Za-z._\\-]*\\s+p[0-9ivxlcdmIVXLCDM]+(?:-[0-9ivxlcdmIVXLCDM]+)?(?:\\s\\(scan p[0-9]+\\))?$"
}
},
"category": {
"type": "string",
"description": "Primary taxonomy bucket.",
"enum": [
"editorial",
"typography",
"layout",
"headings",
"citations",
"numbers",
"punctuation",
"abbreviations",
"links",
"tables",
"figures",
"code",
"frontmatter",
"backmatter",
"accessibility",
"i18n"
]
},
"severity": {
"type": "string",
"description": "Normativity level. MUST blocks release unless downgraded by profile.",
"enum": ["must", "should", "warn"]
},
"applies_to": {
"type": "string",
"description": "Which pipeline stage(s) the rule targets.",
"enum": ["md", "html", "pdf", "all"]
},
"rule_text": {
"type": "string",
"description": "Paraphrased rule statement (no long quotes). If exact wording matters, note: Exact wording required—refer to pointer.",
"minLength": 10,
"maxLength": 800
},
"rationale": {
"type": "string",
"description": "One-line rationale.",
"minLength": 5,
"maxLength": 200
},
"enforcement": {
"type": "string",
"description": "Primary enforcement mechanism.",
"enum": ["lint", "typeset", "postrender", "manual"]
},
"autofix": {
"type": "string",
"description": "Autofix capability, if any.",
"enum": ["none", "rewrite", "reflow", "suggest"]
},
"autofix_notes": {
"type": "string",
"description": "Notes describing what can be fixed and how/when. Keep short; never include book quotes.",
"maxLength": 400
},
"tags": {
"type": "array",
"description": "Compact labels for routing/search/overrides (e.g. manual_checklist=true, widows_orphans, hyphenation).",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 48,
"pattern": "^[a-z0-9][a-z0-9_.:\\-/]*(?:=[a-z0-9_.:\\-/]+)?$"
},
"maxItems": 64
},
"keywords": {
"type": "array",
"description": "Search keywords (human-oriented; not necessarily normalized).",
"items": {
"type": "string",
"minLength": 2,
"maxLength": 48
},
"maxItems": 64
},
"dependencies": {
"type": "array",
"description": "Rule IDs that should be applied/understood first.",
"items": {
"type": "string",
"pattern": "^(CMOS|BRING|HOUSE)\\.[A-Z0-9_]+(?:\\.[A-Z0-9_]+)*$"
},
"maxItems": 32
},
"exceptions": {
"type": "array",
"description": "Free-text exceptions/caveats. Keep concise.",
"items": {
"type": "string",
"minLength": 3,
"maxLength": 240
},
"maxItems": 32
},
"examples_ref": {
"type": "array",
"description": "Optional references to separately stored examples (see spec/examples/README.md).",
"items": {
"type": "string",
"minLength": 6,
"maxLength": 80,
"pattern": "^EX\\.[A-Z0-9_]+\\.[A-Z0-9_]+\\.[0-9]{3,}$"
},
"maxItems": 64
},
"implementation_notes": {
"type": "string",
"description": "Optional short notes for implementers (no quotes).",
"minLength": 3,
"maxLength": 600
},
"status": {
"type": "string",
"description": "Lifecycle state.",
"enum": ["draft", "active", "deprecated"]
}
},
"allOf": [
{
"if": {
"properties": { "autofix": { "enum": ["rewrite", "reflow", "suggest"] } },
"required": ["autofix"]
},
"then": { "properties": { "autofix_notes": { "minLength": 1 } } }
}
]
}

View file

@ -0,0 +1,4 @@
__all__ = ["__version__"]
__version__ = "0.1.0"

431
src/iftypeset/cli.py Normal file
View file

@ -0,0 +1,431 @@
import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path
from iftypeset import __version__
from iftypeset.css_gen import generate_profile_css
from iftypeset.index_builder import build_indexes, write_indexes
from iftypeset.linting import collect_input_paths, lint_paths, manual_checklist
from iftypeset.qa import analyze_html, evaluate_gates, layout_report_dict
from iftypeset.rendering import render_html, render_pdf, renderer_info
from iftypeset.reporting import CoverageError, CoverageReport, build_coverage_report
from iftypeset.spec_loader import SpecError, load_spec
def _cmd_validate_spec(args: argparse.Namespace) -> int:
try:
spec = load_spec(Path(args.spec))
except SpecError as e:
_write_json(Path(args.out) / "spec-validation.json", {"ok": False, "error": str(e)})
print(json.dumps({"ok": False, "error": str(e)}))
return 4
out: dict[str, object] = {
"ok": True,
"spec_root": str(Path(args.spec).resolve()),
"manifest_version": spec.manifest.get("version"),
"profiles": sorted(spec.profiles.keys()),
}
if args.build_indexes:
indexes = build_indexes(spec.rules)
write_indexes(indexes, Path(args.spec) / "indexes")
out["indexes_written"] = True
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
_write_json(out_dir / "spec-validation.json", out)
print(json.dumps(out, indent=2, sort_keys=True))
return 0
def _cmd_report(args: argparse.Namespace) -> int:
try:
spec = load_spec(Path(args.spec))
except SpecError as e:
print(json.dumps({"ok": False, "error": str(e)}))
return 4
try:
report: CoverageReport = build_coverage_report(spec, strict=args.strict)
except CoverageError as e:
print(json.dumps({"ok": False, "error": str(e)}))
return 2
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "coverage-report.json").write_text(
json.dumps(asdict(report), indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
(out_dir / "coverage-summary.md").write_text(report.to_markdown(), encoding="utf-8")
if args.build_indexes:
indexes = build_indexes(spec.rules)
write_indexes(indexes, Path(args.spec) / "indexes")
print(json.dumps({"ok": True, "out_dir": str(out_dir.resolve())}, indent=2, sort_keys=True))
return 0
def _cmd_emit_css(args: argparse.Namespace) -> int:
try:
spec = load_spec(Path(args.spec))
except SpecError as e:
print(json.dumps({"ok": False, "error": str(e)}))
return 4
profile = spec.profiles.get(str(args.profile))
if not profile:
print(json.dumps({"ok": False, "error": f"Unknown profile_id: {args.profile}"}))
return 4
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
css_out = generate_profile_css(profile)
(out_dir / "render.css").write_text(css_out.css, encoding="utf-8")
(out_dir / "typeset-report.json").write_text(
json.dumps(css_out.report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
print(json.dumps({"ok": True, "out_dir": str(out_dir.resolve())}, indent=2, sort_keys=True))
return 0
def _cmd_lint(args: argparse.Namespace) -> int:
try:
spec = load_spec(Path(args.spec))
except SpecError as e:
print(json.dumps({"ok": False, "error": str(e)}))
return 4
profile = spec.profiles.get(str(args.profile))
if not profile:
print(json.dumps({"ok": False, "error": f"Unknown profile_id: {args.profile}"}))
return 4
input_path = Path(args.input)
if not input_path.exists():
print(json.dumps({"ok": False, "error": f"Input path not found: {input_path}"}))
return 4
paths = collect_input_paths(input_path)
if not paths:
print(json.dumps({"ok": False, "error": f"No markdown files found under {input_path}"}))
return 4
result = lint_paths(
paths,
profile_id=str(args.profile),
fix=args.fix,
fix_mode=args.fix_mode,
degraded_ok=args.degraded_ok,
fail_on=args.fail_on,
)
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
_write_json(out_dir / "lint-report.json", result.report)
if result.degraded:
_write_json(out_dir / "degraded-mode-report.json", {"files": result.degraded})
checklist = manual_checklist(spec)
_write_json(out_dir / "manual-checklist.json", {"items": checklist})
(out_dir / "manual-checklist.md").write_text(_manual_checklist_md(checklist), encoding="utf-8")
if args.fix:
if args.fix_mode == "rewrite":
fixed_dir = out_dir / "fixed"
fixed_dir.mkdir(parents=True, exist_ok=True)
for path_str, content in result.fixed_outputs.items():
path = Path(path_str)
rel = path.name if input_path.is_file() else path.relative_to(input_path)
out_path = fixed_dir / rel
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(content, encoding="utf-8")
else:
_write_json(out_dir / "fix-suggestions.json", {"fixes": result.report.get("fixes", [])})
if args.format == "sarif":
_write_json(out_dir / "lint-report.sarif", _to_sarif(result.report))
if args.format == "json":
print(json.dumps(result.report, indent=2, sort_keys=True))
if args.format == "text":
print(_lint_text_summary(result.report))
return 0 if result.report.get("ok") else 2
def _cmd_render_html(args: argparse.Namespace) -> int:
try:
spec = load_spec(Path(args.spec))
except SpecError as e:
print(json.dumps({"ok": False, "error": str(e)}))
return 4
profile = spec.profiles.get(str(args.profile))
if not profile:
print(json.dumps({"ok": False, "error": f"Unknown profile_id: {args.profile}"}))
return 4
input_path = Path(args.input)
if not input_path.exists() or input_path.is_dir():
print(json.dumps({"ok": False, "error": f"Input markdown file not found: {input_path}"}))
return 4
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
result = render_html(input_path, profile, self_contained=args.self_contained)
(out_dir / "render.html").write_text(result.html, encoding="utf-8")
(out_dir / "render.css").write_text(result.css, encoding="utf-8")
_write_json(out_dir / "typeset-report.json", result.typeset_report)
if result.degraded:
_write_json(out_dir / "degraded-mode-report.json", {"files": result.degraded})
summary = {
"ok": True,
"out_dir": str(out_dir.resolve()),
"warnings": result.warnings,
"degraded": result.degraded,
}
print(json.dumps(summary, indent=2, sort_keys=True))
if result.degraded and not args.degraded_ok:
return 2
return 0
def _cmd_render_pdf(args: argparse.Namespace) -> int:
try:
spec = load_spec(Path(args.spec))
except SpecError as e:
print(json.dumps({"ok": False, "error": str(e)}))
return 4
profile = spec.profiles.get(str(args.profile))
if not profile:
print(json.dumps({"ok": False, "error": f"Unknown profile_id: {args.profile}"}))
return 4
input_path = Path(args.input)
if not input_path.exists() or input_path.is_dir():
print(json.dumps({"ok": False, "error": f"Input markdown file not found: {input_path}"}))
return 4
out_dir = Path(args.out)
result = render_pdf(input_path, profile, out_dir, self_contained=args.self_contained)
_write_json(out_dir / "render-log.json", result.log)
if result.ok:
print(json.dumps({"ok": True, "engine": result.engine, "pdf": result.pdf_path}, indent=2, sort_keys=True))
return 0
print(json.dumps({"ok": False, "engine": result.engine, "error": result.log.get("error")}, indent=2, sort_keys=True))
return 3
def _cmd_qa(args: argparse.Namespace) -> int:
try:
spec = load_spec(Path(args.spec))
except SpecError as e:
print(json.dumps({"ok": False, "error": str(e)}))
return 4
profile = spec.profiles.get(str(args.profile))
if not profile:
print(json.dumps({"ok": False, "error": f"Unknown profile_id: {args.profile}"}))
return 4
out_dir = Path(args.out)
html_path = out_dir / "render.html"
if not html_path.exists():
print(json.dumps({"ok": False, "error": f"Missing render.html at {html_path}"}))
return 4
html_text = html_path.read_text(encoding="utf-8")
layout = analyze_html(html_text, profile, analysis_mode="html")
gates_all = spec.quality_gates.get("profiles", {}).get(str(args.profile))
if not gates_all:
print(json.dumps({"ok": False, "error": f"Missing quality gates for profile {args.profile}"}))
return 4
gates = gates_all.get("strict" if args.strict else "default") or {}
qa_report = evaluate_gates(layout.metrics, gates, profile_id=str(args.profile), strict=args.strict)
qa_report["metrics"] = layout.metrics
layout_dict = layout_report_dict(layout)
if not (out_dir / "render.pdf").exists():
layout_dict.setdefault("warnings", []).append("PDF not available; HTML-only analysis used")
_write_json(out_dir / "layout-report.json", layout_dict)
_write_json(out_dir / "qa-report.json", qa_report)
print(json.dumps({"ok": qa_report.get("ok"), "out_dir": str(out_dir.resolve())}, indent=2, sort_keys=True))
return 0 if qa_report.get("ok") else 2
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="iftypeset",
description="Publication-quality typesetting runtime (spec tooling).",
)
parser.add_argument("--version", action="store_true", help="Print tool version and exit.")
sub = parser.add_subparsers(dest="command")
validate = sub.add_parser("validate-spec", help="Validate spec files and rule records.")
validate.add_argument("--spec", default="spec", help="Spec root directory (default: ./spec).")
validate.add_argument("--out", default="out", help="Output directory (default: ./out).")
validate.add_argument("--build-indexes", action="store_true", help="Build and write indexes into spec/indexes/.")
validate.set_defaults(func=_cmd_validate_spec)
report = sub.add_parser("report", help="Generate coverage report from the current spec and rule set.")
report.add_argument("--spec", default="spec", help="Spec root directory (default: ./spec).")
report.add_argument("--out", default="out", help="Output directory (default: ./out).")
report.add_argument("--strict", action="store_true", help="Fail if coverage floors are not met.")
report.add_argument("--build-indexes", action="store_true", help="Build and write indexes into spec/indexes/.")
report.set_defaults(func=_cmd_report)
emit_css = sub.add_parser("emit-css", help="Emit deterministic CSS for a given typeset profile.")
emit_css.add_argument("--spec", default="spec", help="Spec root directory (default: ./spec).")
emit_css.add_argument("--profile", required=True, help="Profile id (e.g., web_pdf, print_pdf).")
emit_css.add_argument("--out", default="out", help="Output directory (default: ./out).")
emit_css.set_defaults(func=_cmd_emit_css)
lint = sub.add_parser("lint", help="Run Markdown lint rules and emit diagnostics.")
lint.add_argument("--spec", default="spec", help="Spec root directory (default: ./spec).")
lint.add_argument("--input", required=True, help="Markdown file or directory.")
lint.add_argument("--out", default="out", help="Output directory (default: ./out).")
lint.add_argument("--profile", required=True, help="Profile id.")
lint.add_argument("--format", default="json", choices=["json", "sarif", "text"], help="Output format.")
lint.add_argument("--fail-on", default="must", choices=["must", "should", "warn"], help="Fail threshold.")
lint.add_argument("--degraded-ok", action="store_true", help="Allow degraded mode without failing.")
lint.add_argument("--fix", action="store_true", help="Apply safe deterministic fixes.")
lint.add_argument("--fix-mode", default="suggest", choices=["suggest", "rewrite"], help="Fix mode.")
lint.set_defaults(func=_cmd_lint)
render_html_cmd = sub.add_parser("render-html", help="Render Markdown to deterministic HTML + CSS.")
render_html_cmd.add_argument("--spec", default="spec", help="Spec root directory (default: ./spec).")
render_html_cmd.add_argument("--input", required=True, help="Markdown file.")
render_html_cmd.add_argument("--out", default="out", help="Output directory (default: ./out).")
render_html_cmd.add_argument("--profile", required=True, help="Profile id.")
render_html_cmd.add_argument("--self-contained", action="store_true", help="Embed local images as data URIs.")
render_html_cmd.add_argument("--degraded-ok", action="store_true", help="Allow degraded mode without failing.")
render_html_cmd.set_defaults(func=_cmd_render_html)
render_pdf_cmd = sub.add_parser("render-pdf", help="Render Markdown to PDF using available engines.")
render_pdf_cmd.add_argument("--spec", default="spec", help="Spec root directory (default: ./spec).")
render_pdf_cmd.add_argument("--input", required=True, help="Markdown file.")
render_pdf_cmd.add_argument("--out", default="out", help="Output directory (default: ./out).")
render_pdf_cmd.add_argument("--profile", required=True, help="Profile id.")
render_pdf_cmd.add_argument("--self-contained", action="store_true", help="Embed local images as data URIs.")
render_pdf_cmd.set_defaults(func=_cmd_render_pdf)
qa = sub.add_parser("qa", help="Run post-render QA gates.")
qa.add_argument("--spec", default="spec", help="Spec root directory (default: ./spec).")
qa.add_argument("--out", default="out", help="Output directory (default: ./out).")
qa.add_argument("--profile", required=True, help="Profile id.")
qa.add_argument("--strict", action="store_true", help="Use strict thresholds.")
qa.set_defaults(func=_cmd_qa)
return parser
def _write_json(path: Path, data: dict[str, object]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def _manual_checklist_md(items: list[dict[str, object]]) -> str:
lines = ["# Manual checklist", ""]
if not items:
lines.append("_No manual checklist items._")
lines.append("")
return "\n".join(lines)
for item in items:
lines.append(f"- {item.get('id')}: {item.get('title')}")
if item.get("rule_text"):
lines.append(f" - {item.get('rule_text')}")
if item.get("source_refs"):
refs = ", ".join([str(r) for r in item.get("source_refs") or []])
lines.append(f" - Source: {refs}")
lines.append("")
return "\n".join(lines)
def _lint_text_summary(report: dict[str, object]) -> str:
summary = report.get("summary", {}) if isinstance(report.get("summary"), dict) else {}
counts = summary.get("diagnostic_counts", {}) if isinstance(summary.get("diagnostic_counts"), dict) else {}
degraded = summary.get("degraded_files", 0)
lines = [
f"Diagnostics: must={counts.get('must', 0)}, should={counts.get('should', 0)}, warn={counts.get('warn', 0)}",
f"Degraded files: {degraded}",
]
return "\n".join(lines)
def _to_sarif(report: dict[str, object]) -> dict[str, object]:
results = []
for diag in report.get("diagnostics", []) or []:
if not isinstance(diag, dict):
continue
severity = diag.get("severity")
level = "warning"
if severity == "must":
level = "error"
elif severity == "warn":
level = "note"
results.append(
{
"ruleId": diag.get("code"),
"level": level,
"message": {"text": diag.get("message")},
"locations": [
{
"physicalLocation": {
"artifactLocation": {"uri": diag.get("path")},
"region": {"startLine": diag.get("line") or 1},
}
}
],
}
)
return {
"version": "2.1.0",
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": [
{
"tool": {"driver": {"name": "iftypeset", "version": __version__}},
"results": results,
}
],
}
def main(argv: list[str] | None = None) -> None:
parser = build_parser()
args = parser.parse_args(argv)
if args.version:
print(
json.dumps(
{
"tool": "iftypeset",
"version": __version__,
"renderer": renderer_info(),
},
indent=2,
sort_keys=True,
)
)
raise SystemExit(0)
if not getattr(args, "func", None):
parser.error("Command required")
try:
rc = int(args.func(args))
except BrokenPipeError:
rc = 1
raise SystemExit(rc)
if __name__ == "__main__":
main(sys.argv[1:])

242
src/iftypeset/css_gen.py Normal file
View file

@ -0,0 +1,242 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class CssOutput:
css: str
report: dict[str, Any]
def generate_profile_css(profile: dict[str, Any]) -> CssOutput:
profile_id = str(profile.get("profile_id") or "").strip()
if not profile_id:
raise ValueError("profile_id is required")
page = profile.get("page") or {}
fonts = profile.get("fonts") or {}
paragraphs = profile.get("paragraphs") or {}
hyphenation = profile.get("hyphenation") or {}
widows_orphans = profile.get("widows_orphans") or {}
headings = profile.get("headings") or {}
code = profile.get("code") or {}
tables = profile.get("tables") or {}
page_size = str(page.get("size") or "Letter")
orientation = str(page.get("orientation") or "portrait")
two_sided = bool(page.get("two_sided") or False)
margins = page.get("margins") or {}
margin_top = str(margins.get("top") or "1in")
margin_bottom = str(margins.get("bottom") or "1in")
margin_inner = str(margins.get("inner") or "1in")
margin_outer = str(margins.get("outer") or "1in")
body_font = fonts.get("body") or {}
heading_font = fonts.get("heading") or {}
mono_font = fonts.get("mono") or {}
body_family = _font_stack(body_font.get("family") or [])
heading_family = _font_stack(heading_font.get("family") or [])
mono_family = _font_stack(mono_font.get("family") or [])
body_size = str(body_font.get("size") or "11pt")
body_line_height = body_font.get("line_height") or 1.45
heading_size = str(heading_font.get("size") or body_size)
mono_size = str(mono_font.get("size") or "10pt")
mono_line_height = mono_font.get("line_height") or 1.35
indent = str(paragraphs.get("indent") or "0")
first_paragraph_indent = str(paragraphs.get("first_paragraph_indent") or "0")
block_spacing = str(paragraphs.get("block_paragraph_spacing") or "0")
hyphens_enabled = bool(hyphenation.get("enabled") or False)
widows = int(widows_orphans.get("widow_lines") or 2)
orphans = int(widows_orphans.get("orphan_lines") or 2)
keep_with_next_lines = int(headings.get("keep_with_next_lines") or 2)
avoid_stranded = bool(headings.get("avoid_stranded_headings") or False)
code_block = (code.get("block") or {}) if isinstance(code, dict) else {}
code_block_size = str(code_block.get("font_size") or mono_size)
code_block_line_height = code_block.get("line_height") or mono_line_height
code_block_wrap = bool(code_block.get("wrap") or False)
table_padding = str(tables.get("cell_padding") or "3pt 6pt")
table_header_repeat = bool(tables.get("header_repeat") or False)
# Build CSS deterministically.
css: list[str] = []
css.append("/* iftypeset profile CSS */")
css.append(f"/* profile_id: {profile_id} */")
css.append("")
css.append(":root {")
css.append(f" --if-body-font: {body_family};")
css.append(f" --if-heading-font: {heading_family};")
css.append(f" --if-mono-font: {mono_family};")
css.append(f" --if-body-size: {body_size};")
css.append(f" --if-heading-size: {heading_size};")
css.append(f" --if-mono-size: {mono_size};")
css.append("}")
css.append("")
# Paged media
css.append("@page {")
css.append(f" size: {page_size} {orientation};")
css.append(f" margin-top: {margin_top};")
css.append(f" margin-bottom: {margin_bottom};")
if two_sided:
css.append("}")
css.append("@page:left {")
css.append(f" margin-left: {margin_inner};")
css.append(f" margin-right: {margin_outer};")
css.append("}")
css.append("@page:right {")
css.append(f" margin-left: {margin_outer};")
css.append(f" margin-right: {margin_inner};")
else:
css.append(f" margin-left: {margin_inner};")
css.append(f" margin-right: {margin_outer};")
css.append("}")
css.append("")
css.append("html {")
css.append(" font-kerning: normal;")
css.append(" text-rendering: geometricPrecision;")
css.append("}")
css.append("")
css.append("body {")
css.append(" font-family: var(--if-body-font);")
css.append(f" font-size: var(--if-body-size);")
css.append(f" line-height: {body_line_height};")
css.append(" color: #111827;")
css.append("}")
css.append("")
css.append("h1, h2, h3, h4, h5, h6 {")
css.append(" font-family: var(--if-heading-font);")
css.append(" font-weight: 650;")
css.append(" break-after: avoid;")
if avoid_stranded:
css.append(" break-inside: avoid;")
css.append("}")
css.append("")
css.append("h1 { font-size: 1.8em; }")
css.append("h2 { font-size: 1.5em; }")
css.append("h3 { font-size: 1.25em; }")
css.append("")
css.append("p {")
if hyphens_enabled:
css.append(" hyphens: auto;")
css.append(f" widows: {widows};")
css.append(f" orphans: {orphans};")
if indent != "0":
css.append(f" text-indent: {indent};")
if block_spacing != "0":
css.append(f" margin-top: {block_spacing};")
css.append(f" margin-bottom: {block_spacing};")
css.append("}")
css.append("")
# First paragraph in a section: no indent when configured.
if first_paragraph_indent == "0" and indent != "0":
css.append("main > p:first-of-type, section > p:first-of-type {")
css.append(" text-indent: 0;")
css.append("}")
css.append("")
css.append("code, pre {")
css.append(" font-family: var(--if-mono-font);")
css.append(f" font-size: {mono_size};")
css.append(f" line-height: {mono_line_height};")
css.append("}")
css.append("")
css.append("pre {")
css.append(f" font-size: {code_block_size};")
css.append(f" line-height: {code_block_line_height};")
css.append(" white-space: pre;")
if code_block_wrap:
css.append(" white-space: pre-wrap;")
css.append(" overflow-wrap: anywhere;")
else:
css.append(" overflow-x: auto;")
css.append("}")
css.append("")
css.append("table {")
css.append(" border-collapse: collapse;")
css.append(" width: 100%;")
css.append("}")
css.append("")
css.append("th, td {")
css.append(f" padding: {table_padding};")
css.append(" border: 1px solid #e5e7eb;")
css.append(" vertical-align: top;")
css.append("}")
css.append("")
if table_header_repeat:
css.append("thead { display: table-header-group; }")
css.append("")
# Approximation: keep headings with at least N following lines by avoiding breaks after heading.
# CSS cannot express "keep_with_next_lines" precisely; this is a pragmatic guard.
if keep_with_next_lines > 0:
css.append("h1, h2, h3 {")
css.append(" page-break-after: avoid;")
css.append("}")
css.append("")
report = {
"profile_id": profile_id,
"page": {
"size": page_size,
"orientation": orientation,
"two_sided": two_sided,
"margins": {"top": margin_top, "bottom": margin_bottom, "inner": margin_inner, "outer": margin_outer},
},
"fonts": {
"body": {"family": body_family, "size": body_size, "line_height": body_line_height},
"heading": {"family": heading_family, "size": heading_size},
"mono": {"family": mono_family, "size": mono_size, "line_height": mono_line_height},
},
"hyphenation": {"enabled": hyphens_enabled},
"widows_orphans": {"widows": widows, "orphans": orphans},
}
return CssOutput(css="\n".join(css).rstrip() + "\n", report=report)
def _font_stack(family: list[Any]) -> str:
parts: list[str] = []
generic = {
"serif",
"sans-serif",
"monospace",
"cursive",
"fantasy",
"system-ui",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded",
"math",
"emoji",
"fangsong",
}
for f in family:
s = str(f).strip()
if not s:
continue
if s.lower() in generic:
parts.append(s.lower())
elif any(ch in s for ch in [" ", "-"]):
parts.append(f'"{s}"')
else:
parts.append(s)
return ", ".join(parts) if parts else "serif"

View file

@ -0,0 +1,67 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from iftypeset.rules import RuleRecord
def _norm_keyword(s: str) -> str:
return " ".join(str(s).strip().lower().split())
@dataclass(frozen=True)
class IndexBundle:
keywords_all: dict[str, list[str]]
source_refs_all: dict[str, list[str]]
category: dict[str, list[str]]
enforcement: dict[str, list[str]]
def build_indexes(rules: list[RuleRecord]) -> IndexBundle:
keywords: dict[str, set[str]] = {}
source_refs: dict[str, set[str]] = {}
by_category: dict[str, set[str]] = {}
by_enforcement: dict[str, set[str]] = {}
for r in rules:
rid = r.id
by_category.setdefault(r.category, set()).add(rid)
by_enforcement.setdefault(r.enforcement, set()).add(rid)
for kw in (r.raw.get("keywords") or []):
k = _norm_keyword(kw)
if not k:
continue
keywords.setdefault(k, set()).add(rid)
for ref in (r.raw.get("source_refs") or []):
ref_s = str(ref).strip()
if not ref_s:
continue
source_refs.setdefault(ref_s, set()).add(rid)
def to_sorted(d: dict[str, set[str]]) -> dict[str, list[str]]:
return {k: sorted(v) for k, v in sorted(d.items(), key=lambda kv: kv[0])}
return IndexBundle(
keywords_all=to_sorted(keywords),
source_refs_all=to_sorted(source_refs),
category=to_sorted(by_category),
enforcement=to_sorted(by_enforcement),
)
def write_indexes(indexes: IndexBundle, indexes_dir: Path) -> None:
indexes_dir.mkdir(parents=True, exist_ok=True)
_write(indexes_dir / "keywords_all.json", indexes.keywords_all)
_write(indexes_dir / "source_refs_all.json", indexes.source_refs_all)
_write(indexes_dir / "category.json", indexes.category)
_write(indexes_dir / "enforcement.json", indexes.enforcement)
def _write(path: Path, data: Any) -> None:
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")

343
src/iftypeset/linting.py Normal file
View file

@ -0,0 +1,343 @@
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
import re
from typing import Any
from iftypeset import __version__
from iftypeset.md_parser import MdDocument, code_fence_lines, parse_markdown, read_markdown
from iftypeset.spec_loader import LoadedSpec
SEVERITY_ORDER = {"warn": 0, "should": 1, "must": 2}
@dataclass(frozen=True)
class LintDiagnostic:
code: str
message: str
severity: str
path: str
line: int | None = None
column: int | None = None
rule_id: str | None = None
@dataclass(frozen=True)
class LintFix:
code: str
description: str
count: int
mode: str
@dataclass(frozen=True)
class LintResult:
report: dict[str, Any]
fixed_outputs: dict[str, str]
degraded: dict[str, list[str]]
def collect_input_paths(input_path: Path) -> list[Path]:
if input_path.is_dir():
return sorted([p for p in input_path.rglob("*.md") if p.is_file()])
return [input_path]
def lint_paths(
input_paths: list[Path],
profile_id: str,
*,
fix: bool = False,
fix_mode: str = "suggest",
degraded_ok: bool = False,
fail_on: str = "must",
) -> LintResult:
diagnostics: list[LintDiagnostic] = []
fixes: list[LintFix] = []
fixed_outputs: dict[str, str] = {}
degraded: dict[str, list[str]] = {}
for path in input_paths:
raw_text, read_reasons = read_markdown(path)
doc = parse_markdown(path, raw_text)
reasons = read_reasons + doc.degraded_reasons
if reasons:
degraded[str(path)] = reasons
diagnostics.extend(_lint_document(doc))
if fix:
fixed_text, applied = apply_fixes(raw_text, fix_mode=fix_mode)
if applied:
fixes.extend(applied)
fixed_outputs[str(path)] = fixed_text
report = _build_report(
diagnostics=diagnostics,
fixes=fixes,
inputs=input_paths,
profile_id=profile_id,
degraded=degraded,
degraded_ok=degraded_ok,
fail_on=fail_on,
)
return LintResult(report=report, fixed_outputs=fixed_outputs, degraded=degraded)
def manual_checklist(spec: LoadedSpec) -> list[dict[str, Any]]:
items: list[dict[str, Any]] = []
for rule in spec.rules:
tags = [str(t) for t in (rule.raw.get("tags") or [])]
if "manual_checklist=true" not in tags:
continue
items.append(
{
"id": rule.id,
"title": rule.raw.get("title"),
"category": rule.raw.get("category"),
"severity": rule.raw.get("severity"),
"rule_text": rule.raw.get("rule_text"),
"source_refs": rule.raw.get("source_refs"),
}
)
return items
def apply_fixes(text: str, *, fix_mode: str) -> tuple[str, list[LintFix]]:
if fix_mode not in {"suggest", "rewrite"}:
raise ValueError("fix_mode must be suggest or rewrite")
lines = text.splitlines()
fenced = code_fence_lines(text)
new_lines, trailing_count = _strip_trailing_whitespace(lines, fenced)
new_lines, link_count = _normalize_link_spacing(new_lines, fenced)
fixes: list[LintFix] = []
if trailing_count:
fixes.append(
LintFix(
code="WS.TRAILING",
description="Trim trailing whitespace",
count=trailing_count,
mode=fix_mode,
)
)
if link_count:
fixes.append(
LintFix(
code="LINK.SPACING",
description="Normalize Markdown link spacing",
count=link_count,
mode=fix_mode,
)
)
out_text = "\n".join(new_lines)
if text.endswith("\n"):
out_text += "\n"
if fix_mode == "suggest":
return text, fixes
return out_text, fixes
def _strip_trailing_whitespace(lines: list[str], fenced: set[int]) -> tuple[list[str], int]:
out: list[str] = []
count = 0
for idx, line in enumerate(lines, start=1):
if idx in fenced:
out.append(line)
continue
stripped = line.rstrip()
if stripped != line:
count += 1
out.append(stripped)
return out, count
def _normalize_link_spacing(lines: list[str], fenced: set[int]) -> tuple[list[str], int]:
out: list[str] = []
count = 0
pattern = re.compile(r"\]\s+\(\s*([^)]+?)\s*\)")
for idx, line in enumerate(lines, start=1):
if idx in fenced:
out.append(line)
continue
new_line, n = pattern.subn(r"](\1)", line)
count += n
out.append(new_line)
return out, count
def _lint_document(doc: MdDocument) -> list[LintDiagnostic]:
diagnostics: list[LintDiagnostic] = []
lines = doc.normalized_source.splitlines()
fenced = code_fence_lines(doc.normalized_source)
for idx, line in enumerate(lines, start=1):
if idx in fenced:
continue
if line.rstrip() != line:
diagnostics.append(
LintDiagnostic(
code="WS.TRAILING",
message="Trailing whitespace",
severity="warn",
path=str(doc.path),
line=idx,
column=len(line),
)
)
for block in doc.blocks:
if block.type in {"paragraph", "blockquote"}:
if re.search(r"\S \S", block.text):
diagnostics.append(
LintDiagnostic(
code="WS.DOUBLE_SPACE",
message="Double space in running text",
severity="should",
path=str(doc.path),
line=block.start_line,
column=1,
)
)
if block.type == "heading" and block.level:
if block.level > 1 and not _prior_heading_level(doc, block.level, block.start_line):
diagnostics.append(
LintDiagnostic(
code="HEADING.SKIP",
message="Heading level skips a level",
severity="should",
path=str(doc.path),
line=block.start_line,
column=1,
)
)
if block.type == "table":
if not any(h for h in block.headers):
diagnostics.append(
LintDiagnostic(
code="TABLE.HEADER.MISSING",
message="Table header row is empty",
severity="warn",
path=str(doc.path),
line=block.start_line,
column=1,
)
)
for idx, line in enumerate(lines, start=1):
if idx in fenced:
continue
for match in re.finditer(r"(https?://\S+|www\.\S+)", line):
token = match.group(1)
if _long_unbroken(token, 80):
diagnostics.append(
LintDiagnostic(
code="LINK.WRAP.RISK",
message="Very long unbroken URL may wrap poorly",
severity="warn",
path=str(doc.path),
line=idx,
column=match.start(1) + 1,
)
)
link_spacing = re.compile(r"\]\s+\(\s*([^)]+?)\s*\)")
for idx, line in enumerate(lines, start=1):
if idx in fenced:
continue
if link_spacing.search(line):
diagnostics.append(
LintDiagnostic(
code="LINK.SPACING",
message="Normalize spacing inside Markdown links",
severity="warn",
path=str(doc.path),
line=idx,
column=1,
)
)
break
return diagnostics
def _prior_heading_level(doc: MdDocument, level: int, line_no: int) -> bool:
for block in doc.blocks:
if block.type != "heading" or block.level is None:
continue
if block.start_line >= line_no:
break
if block.level == level - 1:
return True
return level == 1
def _long_unbroken(text: str, threshold: int) -> bool:
for token in re.findall(r"\S+", text):
if len(token) >= threshold:
return True
return False
def _build_report(
*,
diagnostics: list[LintDiagnostic],
fixes: list[LintFix],
inputs: list[Path],
profile_id: str,
degraded: dict[str, list[str]],
degraded_ok: bool,
fail_on: str,
) -> dict[str, Any]:
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
counts = {"must": 0, "should": 0, "warn": 0}
for diag in diagnostics:
counts[diag.severity] = counts.get(diag.severity, 0) + 1
failed = _fails_threshold(diagnostics, degraded, degraded_ok=degraded_ok, fail_on=fail_on)
return {
"ok": not failed,
"tool": "iftypeset",
"tool_version": __version__,
"generated_at_utc": now,
"profile_id": profile_id,
"inputs": [str(p) for p in inputs],
"fail_on": fail_on,
"degraded_ok": degraded_ok,
"summary": {
"diagnostic_counts": counts,
"fix_counts": {f.code: f.count for f in fixes},
"degraded_files": len(degraded),
},
"degraded": degraded,
"diagnostics": [asdict(d) for d in diagnostics],
"fixes": [asdict(f) for f in fixes],
}
def _fails_threshold(
diagnostics: list[LintDiagnostic],
degraded: dict[str, list[str]],
*,
degraded_ok: bool,
fail_on: str,
) -> bool:
if degraded and not degraded_ok:
return True
if fail_on not in SEVERITY_ORDER:
raise ValueError(f"Unknown fail-on severity: {fail_on}")
threshold = SEVERITY_ORDER[fail_on]
return any(SEVERITY_ORDER.get(d.severity, 0) >= threshold for d in diagnostics)
def evaluate_fail_on(diagnostics: list[LintDiagnostic], fail_on: str) -> bool:
if fail_on not in SEVERITY_ORDER:
raise ValueError(f"Unknown fail-on severity: {fail_on}")
threshold = SEVERITY_ORDER[fail_on]
return any(SEVERITY_ORDER.get(d.severity, 0) >= threshold for d in diagnostics)

329
src/iftypeset/md_parser.py Normal file
View file

@ -0,0 +1,329 @@
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
import re
from typing import Iterable
@dataclass(frozen=True)
class MdBlock:
type: str
start_line: int
end_line: int
text: str = ""
level: int | None = None
items: list[str] = field(default_factory=list)
ordered: bool | None = None
info: str | None = None
headers: list[str] = field(default_factory=list)
rows: list[list[str]] = field(default_factory=list)
@dataclass(frozen=True)
class MdDocument:
path: Path
source: str
blocks: list[MdBlock]
degraded: bool
degraded_reasons: list[str]
normalized_source: str
def read_markdown(path: Path) -> tuple[str, list[str]]:
reasons: list[str] = []
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
text = path.read_text(encoding="utf-8", errors="replace")
reasons.append("invalid_utf8_replaced")
return text, reasons
def normalize_newlines(text: str) -> str:
return text.replace("\r\n", "\n").replace("\r", "\n")
def detect_degraded(text: str) -> list[str]:
reasons: list[str] = []
lines = [ln for ln in text.splitlines()]
fenced = code_fence_lines(text)
content_lines: list[str] = []
for idx, ln in enumerate(lines, start=1):
if idx in fenced:
continue
if not ln.strip():
continue
if re.match(r"^#{1,6}\s+", ln):
continue
if re.match(r"^\s*([-*+]\s+|\d+\.\s+)", ln):
continue
if ln.lstrip().startswith(">"):
continue
if "|" in ln and re.match(r"^\s*\|", ln):
continue
content_lines.append(ln.strip())
if content_lines:
lengths = sorted(len(ln) for ln in content_lines)
short = [l for l in lengths if l < 35]
ratio = len(short) / max(len(lengths), 1)
median = lengths[len(lengths) // 2]
if len(lengths) >= 20 and ((ratio > 0.8 and median < 40) or (ratio > 0.6 and median < 30)):
reasons.append("hard_wrap_density")
if not any(re.match(r"^#{1,6}\s+", ln) for ln in lines):
reasons.append("missing_headings")
return reasons
def unwrap_hard_wrapped_paragraphs(text: str) -> str:
lines = text.splitlines()
out_lines: list[str] = []
buf: list[str] = []
def flush_buf() -> None:
if not buf:
return
joined = ""
for i, ln in enumerate(buf):
ln = ln.strip()
if i == 0:
joined = ln
continue
if joined.endswith("-") and ln and ln[0].islower():
joined = joined[:-1] + ln
else:
joined = joined + " " + ln
out_lines.append(joined)
buf.clear()
for ln in lines:
if not ln.strip():
flush_buf()
out_lines.append("")
continue
if re.match(r"^\s*(```|~~~)", ln) or re.match(r"^\s*([-*+]\s+|\d+\.\s+)", ln):
flush_buf()
out_lines.append(ln.rstrip())
continue
if re.match(r"^#{1,6}\s+", ln) or ln.lstrip().startswith(">"):
flush_buf()
out_lines.append(ln.rstrip())
continue
if "|" in ln and re.match(r"^\s*\|?\s*[-:]{3,}", ln):
flush_buf()
out_lines.append(ln.rstrip())
continue
buf.append(ln)
flush_buf()
return "\n".join(out_lines)
def parse_markdown(path: Path, text: str) -> MdDocument:
original = normalize_newlines(text)
reasons = detect_degraded(original)
normalized = original
if reasons:
normalized = unwrap_hard_wrapped_paragraphs(original)
blocks = _parse_blocks(normalized)
return MdDocument(
path=path,
source=original,
blocks=blocks,
degraded=bool(reasons),
degraded_reasons=reasons,
normalized_source=normalized,
)
def _parse_blocks(text: str) -> list[MdBlock]:
lines = text.splitlines()
blocks: list[MdBlock] = []
i = 0
while i < len(lines):
line = lines[i]
if not line.strip():
i += 1
continue
if _is_code_fence(line):
blocks.append(_parse_code_block(lines, i))
i = blocks[-1].end_line
continue
heading = _parse_heading(line, i)
if heading:
blocks.append(heading)
i += 1
continue
table = _parse_table(lines, i)
if table:
blocks.append(table)
i = table.end_line
continue
lst = _parse_list(lines, i)
if lst:
blocks.append(lst)
i = lst.end_line
continue
if line.lstrip().startswith(">"):
blocks.append(_parse_blockquote(lines, i))
i = blocks[-1].end_line
continue
blocks.append(_parse_paragraph(lines, i))
i = blocks[-1].end_line
return blocks
def _is_code_fence(line: str) -> bool:
return bool(re.match(r"^\s*(```|~~~)", line))
def _parse_code_block(lines: list[str], start: int) -> MdBlock:
fence = lines[start]
m = re.match(r"^\s*(```|~~~)(.*)$", fence)
info = m.group(2).strip() if m else ""
i = start + 1
code_lines: list[str] = []
while i < len(lines):
if _is_code_fence(lines[i]):
i += 1
break
code_lines.append(lines[i])
i += 1
text = "\n".join(code_lines)
return MdBlock(type="code", start_line=start + 1, end_line=i, text=text, info=info)
def _parse_heading(line: str, idx: int) -> MdBlock | None:
m = re.match(r"^(#{1,6})\s+(.*)$", line)
if not m:
return None
level = len(m.group(1))
text = m.group(2).strip()
return MdBlock(type="heading", start_line=idx + 1, end_line=idx + 1, text=text, level=level)
def _parse_list(lines: list[str], start: int) -> MdBlock | None:
m = re.match(r"^\s*([-*+])\s+(.+)$", lines[start])
ordered_m = re.match(r"^\s*(\d+)\.\s+(.+)$", lines[start])
if not m and not ordered_m:
return None
items: list[str] = []
ordered = False
i = start
while i < len(lines):
line = lines[i]
m = re.match(r"^\s*([-*+])\s+(.+)$", line)
ordered_m = re.match(r"^\s*(\d+)\.\s+(.+)$", line)
if m:
items.append(m.group(2).strip())
elif ordered_m:
ordered = True
items.append(ordered_m.group(2).strip())
else:
break
i += 1
return MdBlock(
type="list",
start_line=start + 1,
end_line=i,
items=items,
ordered=ordered,
)
def _parse_table(lines: list[str], start: int) -> MdBlock | None:
if start + 1 >= len(lines):
return None
header = lines[start]
sep = lines[start + 1]
if "|" not in header:
return None
if not _is_table_separator(sep):
return None
headers = _split_pipes(header)
rows: list[list[str]] = []
i = start + 2
while i < len(lines):
if "|" not in lines[i] or not lines[i].strip():
break
if _is_code_fence(lines[i]) or re.match(r"^#{1,6}\s+", lines[i]):
break
rows.append(_split_pipes(lines[i]))
i += 1
return MdBlock(
type="table",
start_line=start + 1,
end_line=i,
headers=headers,
rows=rows,
)
def _is_table_separator(line: str) -> bool:
parts = _split_pipes(line)
if len(parts) < 2:
return False
for p in parts:
if not re.match(r"^:?-{3,}:?$", p.strip()):
return False
return True
def _split_pipes(line: str) -> list[str]:
stripped = line.strip()
if stripped.startswith("|"):
stripped = stripped[1:]
if stripped.endswith("|"):
stripped = stripped[:-1]
return [p.strip() for p in stripped.split("|")]
def _parse_blockquote(lines: list[str], start: int) -> MdBlock:
i = start
parts: list[str] = []
while i < len(lines):
line = lines[i]
if not line.lstrip().startswith(">"):
break
parts.append(line.lstrip()[1:].lstrip())
i += 1
text = " ".join([p for p in parts if p])
return MdBlock(type="blockquote", start_line=start + 1, end_line=i, text=text)
def _parse_paragraph(lines: list[str], start: int) -> MdBlock:
i = start
parts: list[str] = []
while i < len(lines):
line = lines[i]
if not line.strip():
break
if _is_code_fence(line) or re.match(r"^#{1,6}\s+", line) or _parse_list(lines, i) or _parse_table(lines, i):
break
if line.lstrip().startswith(">"):
break
parts.append(line.strip())
i += 1
text = " ".join(parts)
return MdBlock(type="paragraph", start_line=start + 1, end_line=i, text=text)
def code_fence_lines(text: str) -> set[int]:
lines = text.splitlines()
in_fence = False
fenced_lines: set[int] = set()
for i, line in enumerate(lines, start=1):
if _is_code_fence(line):
in_fence = not in_fence
fenced_lines.add(i)
continue
if in_fence:
fenced_lines.add(i)
return fenced_lines
def iter_blocks(blocks: Iterable[MdBlock], type_name: str) -> Iterable[MdBlock]:
for b in blocks:
if b.type == type_name:
yield b

344
src/iftypeset/qa.py Normal file
View file

@ -0,0 +1,344 @@
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from html.parser import HTMLParser
from pathlib import Path
import re
from typing import Any
from iftypeset import __version__
@dataclass(frozen=True)
class LayoutIncident:
kind: str
detail: str
context: str | None = None
@dataclass(frozen=True)
class LayoutReport:
metrics: dict[str, int]
incidents: list[LayoutIncident]
warnings: list[str]
profile_id: str
analysis_mode: str
@dataclass
class _Block:
type: str
word_count: int
text: str = ""
level: int | None = None
line_count: int = 0
class HtmlCollector(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.headings: list[tuple[int, str]] = []
self.links: list[tuple[str, str]] = []
self.code_blocks: list[str] = []
self.tables: list[dict[str, int]] = []
self.blocks: list[_Block] = []
self._heading_level: int | None = None
self._heading_text: list[str] = []
self._link_href: str | None = None
self._link_text: list[str] = []
self._in_pre = False
self._code_text: list[str] = []
self._in_paragraph = False
self._paragraph_text: list[str] = []
self._in_list_item = False
self._list_text: list[str] = []
self._in_table = False
self._table_rows: list[int] = []
self._table_cell_lengths: list[int] = []
self._table_word_count = 0
self._current_row_cols = 0
self._cell_text: list[str] | None = None
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag in {"h1", "h2", "h3", "h4", "h5", "h6"}:
self._heading_level = int(tag[1])
self._heading_text = []
if tag == "a":
href = ""
for k, v in attrs:
if k == "href" and v:
href = v
self._link_href = href
self._link_text = []
if tag == "pre":
self._in_pre = True
self._code_text = []
if tag == "p":
self._in_paragraph = True
self._paragraph_text = []
if tag == "li":
self._in_list_item = True
self._list_text = []
if tag == "table":
self._in_table = True
self._table_rows = []
self._table_cell_lengths = []
self._table_word_count = 0
self._current_row_cols = 0
if tag == "tr" and self._in_table:
self._current_row_cols = 0
if tag in {"td", "th"} and self._in_table:
self._cell_text = []
def handle_endtag(self, tag: str) -> None:
if tag in {"h1", "h2", "h3", "h4", "h5", "h6"} and self._heading_level is not None:
text = " ".join(self._heading_text).strip()
self.headings.append((self._heading_level, text))
self.blocks.append(_Block(type="heading", word_count=0, text=text, level=self._heading_level))
self._heading_level = None
self._heading_text = []
if tag == "a" and self._link_href is not None:
text = " ".join(self._link_text).strip()
self.links.append((self._link_href, text))
self._link_href = None
self._link_text = []
if tag == "pre" and self._in_pre:
text = "".join(self._code_text)
self.code_blocks.append(text)
wc = _word_count(text)
line_count = len([ln for ln in text.splitlines() if ln.strip()]) or 1
self.blocks.append(_Block(type="code", word_count=wc, text=text, line_count=line_count))
self._in_pre = False
self._code_text = []
if tag == "p" and self._in_paragraph:
text = " ".join(self._paragraph_text).strip()
wc = _word_count(text)
self.blocks.append(_Block(type="paragraph", word_count=wc, text=text))
self._in_paragraph = False
self._paragraph_text = []
if tag == "li" and self._in_list_item:
text = " ".join(self._list_text).strip()
wc = _word_count(text)
self.blocks.append(_Block(type="list_item", word_count=wc, text=text))
self._in_list_item = False
self._list_text = []
if tag in {"td", "th"} and self._in_table and self._cell_text is not None:
text = " ".join(self._cell_text).strip()
self._table_cell_lengths.append(len(text))
self._table_word_count += _word_count(text)
self._current_row_cols += 1
self._cell_text = None
if tag == "tr" and self._in_table:
self._table_rows.append(self._current_row_cols)
self._current_row_cols = 0
if tag == "table" and self._in_table:
max_cols = max(self._table_rows) if self._table_rows else 0
max_cell = max(self._table_cell_lengths) if self._table_cell_lengths else 0
row_count = len(self._table_rows)
self.tables.append({"max_cols": max_cols, "max_cell_len": max_cell, "rows": row_count})
self.blocks.append(_Block(type="table", word_count=self._table_word_count, line_count=row_count))
self._in_table = False
self._table_rows = []
self._table_cell_lengths = []
self._table_word_count = 0
def handle_data(self, data: str) -> None:
if self._heading_level is not None:
self._heading_text.append(data)
if self._link_href is not None:
self._link_text.append(data)
if self._in_pre:
self._code_text.append(data)
if self._in_paragraph:
self._paragraph_text.append(data)
if self._in_list_item:
self._list_text.append(data)
if self._cell_text is not None:
self._cell_text.append(data)
def analyze_html(html_text: str, profile: dict[str, Any], *, analysis_mode: str = "html") -> LayoutReport:
collector = HtmlCollector()
collector.feed(html_text)
measure = profile.get("measure_targets") or {}
body_chars = (measure.get("body_chars_per_line") or {}).get("max") or 75
body_chars = int(body_chars)
link_threshold = max(80, body_chars)
code_threshold = max(90, body_chars + 15)
table_max_cols = 6
table_max_cell = 40
incidents: list[LayoutIncident] = []
link_wrap = 0
for href, text in collector.links:
token = text or href
if _long_unbroken(token, link_threshold):
link_wrap += 1
incidents.append(LayoutIncident(kind="link_wrap", detail=href, context=token[:120]))
code_overflow = 0
for code in collector.code_blocks:
max_len = max((len(line) for line in code.splitlines()), default=0)
if max_len > code_threshold:
code_overflow += 1
incidents.append(LayoutIncident(kind="code_overflow", detail=f"line_len={max_len}"))
table_overflow = 0
for table in collector.tables:
if table.get("max_cols", 0) > table_max_cols or table.get("max_cell_len", 0) > table_max_cell:
table_overflow += 1
incidents.append(LayoutIncident(kind="table_overflow", detail=str(table)))
heading_errors, heading_incidents = _heading_numbering_errors(collector.headings)
incidents.extend(heading_incidents)
stranded, stranded_incidents = _stranded_headings(collector.blocks, profile)
incidents.extend(stranded_incidents)
overfull_lines = _overfull_lines(collector.blocks, body_chars)
metrics = {
"max_widows_per_10_pages": 0,
"max_orphans_per_10_pages": 0,
"max_stranded_headings": stranded,
"max_overfull_lines": overfull_lines,
"max_table_overflow_incidents": table_overflow,
"max_code_overflow_incidents": code_overflow,
"max_link_wrap_incidents": link_wrap,
"max_heading_numbering_errors": heading_errors,
"max_citation_format_errors": 0,
}
return LayoutReport(
metrics=metrics,
incidents=incidents,
warnings=[],
profile_id=str(profile.get("profile_id") or ""),
analysis_mode=analysis_mode,
)
def evaluate_gates(
metrics: dict[str, int],
gates: dict[str, int],
*,
profile_id: str,
strict: bool,
) -> dict[str, Any]:
results: dict[str, Any] = {}
failed: list[str] = []
for metric, limit in gates.items():
count = metrics.get(metric, 0)
status = "pass" if count <= limit else "fail"
results[metric] = {"count": count, "max": limit, "status": status}
if status == "fail":
failed.append(metric)
return {
"ok": not failed,
"profile_id": profile_id,
"strict": strict,
"gates": results,
"failed": failed,
}
def layout_report_dict(report: LayoutReport) -> dict[str, Any]:
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
return {
"ok": True,
"tool": "iftypeset",
"tool_version": __version__,
"generated_at_utc": now,
"profile_id": report.profile_id,
"analysis_mode": report.analysis_mode,
"metrics": report.metrics,
"incidents": [asdict(i) for i in report.incidents],
"warnings": report.warnings,
}
def _heading_numbering_errors(headings: list[tuple[int, str]]) -> tuple[int, list[LayoutIncident]]:
errors = 0
incidents: list[LayoutIncident] = []
last_numbers: list[int] = []
for level, text in headings:
m = re.match(r"^(\d+(?:\.\d+)*)\s+", text)
if not m:
continue
parts = [int(p) for p in m.group(1).split(".")]
if not last_numbers:
last_numbers = parts
continue
if len(parts) == 1:
if parts[0] <= last_numbers[0]:
errors += 1
incidents.append(LayoutIncident(kind="heading_numbering", detail=text))
last_numbers = parts
continue
prefix = parts[:-1]
if prefix != last_numbers[: len(prefix)]:
errors += 1
incidents.append(LayoutIncident(kind="heading_numbering", detail=text))
last_numbers = parts
continue
prev = last_numbers[len(parts) - 1] if len(last_numbers) >= len(parts) else 0
if parts[-1] != prev + 1 and not (prev == 0 and parts[-1] == 1):
errors += 1
incidents.append(LayoutIncident(kind="heading_numbering", detail=text))
last_numbers = parts
return errors, incidents
def _stranded_headings(blocks: list[_Block], profile: dict[str, Any]) -> tuple[int, list[LayoutIncident]]:
keep_lines = int((profile.get("headings") or {}).get("keep_with_next_lines") or 2)
measure = profile.get("measure_targets") or {}
ideal_chars = (measure.get("body_chars_per_line") or {}).get("ideal") or 66
words_per_line = max(8, int(int(ideal_chars) / 6))
stranded = 0
incidents: list[LayoutIncident] = []
for i, block in enumerate(blocks):
if block.type != "heading":
continue
content_lines = 0
for nxt in blocks[i + 1 :]:
if nxt.type == "heading":
break
if nxt.type in {"code", "table"}:
content_lines += max(keep_lines, nxt.line_count or keep_lines)
elif nxt.type == "list_item":
content_lines += 1
else:
wc = max(0, int(nxt.word_count))
content_lines += max(1, (wc + words_per_line - 1) // words_per_line) if wc else 0
if content_lines >= keep_lines:
break
if content_lines < keep_lines:
stranded += 1
incidents.append(LayoutIncident(kind="stranded_heading", detail=block.text))
return stranded, incidents
def _overfull_lines(blocks: list[_Block], max_chars: int) -> int:
count = 0
for block in blocks:
if block.type not in {"paragraph", "list_item"}:
continue
for token in re.findall(r"\S+", block.text):
if len(token) > max_chars:
count += 1
break
return count
def _long_unbroken(text: str, threshold: int) -> bool:
for token in re.findall(r"\S+", text):
if len(token) >= threshold:
return True
return False
def _word_count(text: str) -> int:
return len([t for t in re.split(r"\s+", text.strip()) if t])

384
src/iftypeset/rendering.py Normal file
View file

@ -0,0 +1,384 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import base64
import html
import importlib
import importlib.util
import json
import mimetypes
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
from typing import Any
from iftypeset import __version__
from iftypeset.css_gen import generate_profile_css
from iftypeset.md_parser import MdDocument, parse_markdown, read_markdown
@dataclass(frozen=True)
class HtmlRenderResult:
html: str
css: str
warnings: list[str]
degraded: dict[str, list[str]]
typeset_report: dict[str, Any]
@dataclass(frozen=True)
class PdfRenderResult:
ok: bool
engine: str | None
pdf_path: str | None
log: dict[str, Any]
def render_html(
input_path: Path,
profile: dict[str, Any],
*,
self_contained: bool = False,
) -> HtmlRenderResult:
raw_text, read_reasons = read_markdown(input_path)
doc = parse_markdown(input_path, raw_text)
degraded: dict[str, list[str]] = {}
reasons = read_reasons + doc.degraded_reasons
if reasons:
degraded[str(input_path)] = reasons
css_out = generate_profile_css(profile)
warnings: list[str] = []
html_out = _render_doc(doc, base_path=input_path.parent, self_contained=self_contained, warnings=warnings)
return HtmlRenderResult(
html=html_out,
css=css_out.css,
warnings=warnings,
degraded=degraded,
typeset_report=css_out.report,
)
def detect_pdf_engines() -> list[dict[str, Any]]:
engines: list[dict[str, Any]] = []
if importlib.util.find_spec("playwright"):
engines.append({"name": "playwright", "detail": "python-module"})
chromium_path = _find_executable(["chromium", "chromium-browser", "google-chrome", "chrome"])
if chromium_path:
engines.append({"name": "chromium", "detail": chromium_path})
wk_path = _find_executable(["wkhtmltopdf"])
if wk_path:
engines.append({"name": "wkhtmltopdf", "detail": wk_path})
if importlib.util.find_spec("weasyprint"):
engines.append({"name": "weasyprint", "detail": "python-module"})
return engines
def renderer_info() -> dict[str, Any]:
engines = detect_pdf_engines()
return {"pdf_engines_detected": engines, "pdf_engine_versions": _engine_versions(engines)}
def render_pdf(
input_path: Path,
profile: dict[str, Any],
out_dir: Path,
*,
self_contained: bool = False,
) -> PdfRenderResult:
out_dir.mkdir(parents=True, exist_ok=True)
html_result = render_html(input_path, profile, self_contained=self_contained)
html_path = out_dir / "render.html"
css_path = out_dir / "render.css"
html_path.write_text(html_result.html, encoding="utf-8")
css_path.write_text(html_result.css, encoding="utf-8")
(out_dir / "typeset-report.json").write_text(
json.dumps(html_result.typeset_report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
engines = detect_pdf_engines()
log = _base_render_log(engines, html_result.warnings)
if not engines:
log["ok"] = False
log["error"] = "No PDF renderer detected. Install playwright, chromium, wkhtmltopdf, or weasyprint."
return PdfRenderResult(ok=False, engine=None, pdf_path=None, log=log)
engine = engines[0]["name"]
pdf_path = out_dir / "render.pdf"
log["engine"] = engine
log["engine_versions"] = _engine_versions(engines)
log["html_path"] = str(html_path)
log["pdf_path"] = str(pdf_path)
try:
if engine == "playwright":
_render_pdf_playwright(html_path, pdf_path)
elif engine == "chromium":
_render_pdf_chromium(html_path, pdf_path, engines[0]["detail"])
elif engine == "wkhtmltopdf":
_render_pdf_wkhtmltopdf(html_path, pdf_path, engines[0]["detail"])
elif engine == "weasyprint":
_render_pdf_weasyprint(html_path, pdf_path)
else:
raise RuntimeError(f"Unknown engine: {engine}")
except Exception as e: # noqa: BLE001
log["ok"] = False
log["error"] = str(e)
return PdfRenderResult(ok=False, engine=engine, pdf_path=None, log=log)
log["ok"] = True
return PdfRenderResult(ok=True, engine=engine, pdf_path=str(pdf_path), log=log)
def _render_doc(
doc: MdDocument,
*,
base_path: Path,
self_contained: bool,
warnings: list[str],
) -> str:
title = _doc_title(doc)
heading_ids: dict[str, int] = {}
body_lines: list[str] = []
body_lines.append("<main>")
for block in doc.blocks:
if block.type == "heading":
level = block.level or 1
slug = _unique_slug(block.text, heading_ids)
body_lines.append(f"<h{level} id=\"{slug}\">{_render_inline(block.text, base_path, self_contained, warnings)}</h{level}>")
continue
if block.type == "paragraph":
body_lines.append(f"<p>{_render_inline(block.text, base_path, self_contained, warnings)}</p>")
continue
if block.type == "list":
tag = "ol" if block.ordered else "ul"
body_lines.append(f"<{tag}>")
for item in block.items:
body_lines.append(f" <li>{_render_inline(item, base_path, self_contained, warnings)}</li>")
body_lines.append(f"</{tag}>")
continue
if block.type == "code":
lang = block.info.strip()
class_attr = f" class=\"language-{html.escape(lang)}\"" if lang else ""
body_lines.append(f"<pre><code{class_attr}>{html.escape(block.text)}</code></pre>")
continue
if block.type == "blockquote":
body_lines.append(f"<blockquote><p>{_render_inline(block.text, base_path, self_contained, warnings)}</p></blockquote>")
continue
if block.type == "table":
body_lines.append("<table>")
body_lines.append(" <thead>")
body_lines.append(" <tr>")
for h in block.headers:
body_lines.append(f" <th>{_render_inline(h, base_path, self_contained, warnings)}</th>")
body_lines.append(" </tr>")
body_lines.append(" </thead>")
body_lines.append(" <tbody>")
for row in block.rows:
body_lines.append(" <tr>")
for cell in row:
body_lines.append(f" <td>{_render_inline(cell, base_path, self_contained, warnings)}</td>")
body_lines.append(" </tr>")
body_lines.append(" </tbody>")
body_lines.append("</table>")
continue
body_lines.append("</main>")
html_lines: list[str] = []
html_lines.append("<!doctype html>")
html_lines.append("<html lang=\"en\">")
html_lines.append("<head>")
html_lines.append(" <meta charset=\"utf-8\">")
html_lines.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">")
html_lines.append(f" <title>{html.escape(title)}</title>")
html_lines.append(" <link rel=\"stylesheet\" href=\"render.css\">")
html_lines.append("</head>")
html_lines.append("<body>")
html_lines.extend([" " + line for line in body_lines])
html_lines.append("</body>")
html_lines.append("</html>")
return "\n".join(html_lines).rstrip() + "\n"
def _doc_title(doc: MdDocument) -> str:
for block in doc.blocks:
if block.type == "heading" and block.level == 1:
return block.text.strip() or doc.path.stem
return doc.path.stem
def _unique_slug(text: str, used: dict[str, int]) -> str:
slug = _slugify(text)
if slug not in used:
used[slug] = 1
return slug
used[slug] += 1
return f"{slug}-{used[slug]}"
def _slugify(text: str) -> str:
slug = re.sub(r"[^a-z0-9\\s-]", "", text.lower())
slug = re.sub(r"\\s+", "-", slug.strip())
return slug or "section"
def _render_inline(text: str, base_path: Path, self_contained: bool, warnings: list[str]) -> str:
out: list[str] = []
i = 0
while i < len(text):
if text[i] == "`":
j = text.find("`", i + 1)
if j == -1:
out.append(html.escape(text[i:]))
break
code_text = text[i + 1 : j]
out.append(f"<code>{html.escape(code_text)}</code>")
i = j + 1
continue
if text[i] == "!" and i + 1 < len(text) and text[i + 1] == "[":
img = _parse_bracket_link(text, i + 1)
if img:
alt, url, end_idx = img
src = _resolve_image_src(url, base_path, self_contained, warnings)
out.append(f"<img src=\"{html.escape(src)}\" alt=\"{html.escape(alt)}\">")
i = end_idx
continue
if text[i] == "[":
link = _parse_bracket_link(text, i)
if link:
label, url, end_idx = link
out.append(f"<a href=\"{html.escape(url)}\">{html.escape(label)}</a>")
i = end_idx
continue
out.append(html.escape(text[i]))
i += 1
return "".join(out)
def _parse_bracket_link(text: str, idx: int) -> tuple[str, str, int] | None:
if text[idx] != "[":
return None
close = text.find("]", idx + 1)
if close == -1 or close + 1 >= len(text) or text[close + 1] != "(":
return None
end = text.find(")", close + 2)
if end == -1:
return None
label = text[idx + 1 : close]
url = text[close + 2 : end].strip()
return label, url, end + 1
def _resolve_image_src(url: str, base_path: Path, self_contained: bool, warnings: list[str]) -> str:
if not self_contained:
return url
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*:", url):
return url
img_path = (base_path / url).resolve()
if not img_path.exists() or not img_path.is_file():
warnings.append(f"Image not found for embedding: {url}")
return url
mime = mimetypes.guess_type(str(img_path))[0] or "application/octet-stream"
data = base64.b64encode(img_path.read_bytes()).decode("ascii")
return f"data:{mime};base64,{data}"
def _find_executable(candidates: list[str]) -> str | None:
for name in candidates:
path = shutil.which(name)
if path:
return path
return None
def _render_pdf_playwright(html_path: Path, pdf_path: Path) -> None:
from playwright.sync_api import sync_playwright # type: ignore
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(html_path.resolve().as_uri())
page.pdf(path=str(pdf_path), print_background=True)
browser.close()
def _render_pdf_chromium(html_path: Path, pdf_path: Path, chromium_path: str) -> None:
uri = html_path.resolve().as_uri()
cmd = [
chromium_path,
"--headless",
"--disable-gpu",
"--print-to-pdf=" + str(pdf_path),
"--print-to-pdf-no-header",
"--allow-file-access-from-files",
uri,
]
if os.geteuid() == 0:
cmd.insert(1, "--no-sandbox")
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def _render_pdf_wkhtmltopdf(html_path: Path, pdf_path: Path, wk_path: str) -> None:
cmd = [wk_path, "--quiet", str(html_path.resolve()), str(pdf_path)]
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def _render_pdf_weasyprint(html_path: Path, pdf_path: Path) -> None:
from weasyprint import HTML # type: ignore
HTML(filename=str(html_path.resolve())).write_pdf(str(pdf_path))
def _base_render_log(engines: list[dict[str, Any]], warnings: list[str]) -> dict[str, Any]:
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
return {
"ok": False,
"tool": "iftypeset",
"tool_version": __version__,
"generated_at_utc": now,
"engine": None,
"engines_detected": engines,
"warnings": warnings,
"python": sys.version.split()[0],
}
def _engine_versions(engines: list[dict[str, Any]]) -> dict[str, str]:
versions: dict[str, str] = {}
for entry in engines:
name = entry.get("name")
detail = entry.get("detail")
if name == "chromium" and isinstance(detail, str):
versions[name] = _run_version_cmd([detail, "--version"])
elif name == "wkhtmltopdf" and isinstance(detail, str):
versions[name] = _run_version_cmd([detail, "--version"])
elif name == "playwright":
versions[name] = _module_version("playwright")
elif name == "weasyprint":
versions[name] = _module_version("weasyprint")
return versions
def _run_version_cmd(cmd: list[str]) -> str:
try:
out = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return out.stdout.strip() or out.stderr.strip()
except Exception: # noqa: BLE001
return "unknown"
def _module_version(module_name: str) -> str:
try:
mod = importlib.import_module(module_name)
return getattr(mod, "__version__", "unknown")
except Exception: # noqa: BLE001
return "unknown"

104
src/iftypeset/reporting.py Normal file
View file

@ -0,0 +1,104 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from iftypeset.spec_loader import LoadedSpec
class CoverageError(RuntimeError):
pass
@dataclass(frozen=True)
class CoverageCounts:
total_rules: int
by_category: dict[str, int]
by_enforcement: dict[str, int]
by_severity: dict[str, int]
@dataclass(frozen=True)
class CoverageReport:
tool: str
tool_version: str
generated_at_utc: str
spec_root: str
manifest_version: str | None
profiles: list[str]
counts: CoverageCounts
notes: list[str] = field(default_factory=list)
def to_markdown(self) -> str:
lines: list[str] = []
lines.append("# iftypeset coverage report\n")
lines.append(f"- Generated (UTC): `{self.generated_at_utc}`")
lines.append(f"- Spec root: `{self.spec_root}`")
if self.manifest_version:
lines.append(f"- Manifest version: `{self.manifest_version}`")
lines.append(f"- Profiles: {', '.join(f'`{p}`' for p in self.profiles)}\n")
lines.append("## Counts\n")
lines.append(f"- Total rules: `{self.counts.total_rules}`")
def section(title: str, d: dict[str, int]) -> None:
lines.append(f"\n### {title}\n")
if not d:
lines.append("_None_\n")
return
for k, v in sorted(d.items(), key=lambda kv: kv[0]):
lines.append(f"- `{k}`: `{v}`")
lines.append("")
section("By Category", self.counts.by_category)
section("By Enforcement", self.counts.by_enforcement)
section("By Severity", self.counts.by_severity)
if self.notes:
lines.append("## Notes\n")
for n in self.notes:
lines.append(f"- {n}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def build_coverage_report(spec: LoadedSpec, *, strict: bool) -> CoverageReport:
by_category: dict[str, int] = {}
by_enforcement: dict[str, int] = {}
by_severity: dict[str, int] = {}
for r in spec.rules:
by_category[r.category] = by_category.get(r.category, 0) + 1
by_enforcement[r.enforcement] = by_enforcement.get(r.enforcement, 0) + 1
by_severity[r.severity] = by_severity.get(r.severity, 0) + 1
notes: list[str] = []
if not spec.rules:
notes.append("No rule batches found under spec/rules/. Phase 2 extraction has not been applied yet.")
# This is intentionally conservative: without rule implementation handlers, we cannot compute implemented coverage.
if strict and not spec.rules:
raise CoverageError("Strict coverage requested, but no rules are present.")
counts = CoverageCounts(
total_rules=len(spec.rules),
by_category=by_category,
by_enforcement=by_enforcement,
by_severity=by_severity,
)
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
from iftypeset import __version__
return CoverageReport(
tool="iftypeset",
tool_version=__version__,
generated_at_utc=now,
spec_root=str(spec.spec_root),
manifest_version=spec.manifest.get("version"),
profiles=sorted(spec.profiles.keys()),
counts=counts,
notes=notes,
)

78
src/iftypeset/rules.py Normal file
View file

@ -0,0 +1,78 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
from jsonschema import Draft202012Validator
class RulesError(RuntimeError):
pass
@dataclass(frozen=True)
class RuleRecord:
raw: dict[str, Any]
source_path: str
line_no: int
@property
def id(self) -> str:
return str(self.raw.get("id", ""))
@property
def category(self) -> str:
return str(self.raw.get("category", ""))
@property
def enforcement(self) -> str:
return str(self.raw.get("enforcement", ""))
@property
def severity(self) -> str:
return str(self.raw.get("severity", ""))
def _iter_ndjson_files(rules_root: Path) -> Iterable[Path]:
if not rules_root.exists():
return []
return sorted([p for p in rules_root.rglob("*.ndjson") if p.is_file()])
def load_rules(rules_root: Path, *, validator: Draft202012Validator) -> list[RuleRecord]:
rules: list[RuleRecord] = []
if not rules_root.exists():
return rules
seen_ids: dict[str, str] = {}
for path in _iter_ndjson_files(rules_root):
rel = str(path)
for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
if not line.strip():
continue
try:
obj = json.loads(line)
except json.JSONDecodeError as e:
raise RulesError(f"Invalid NDJSON ({rel}:{i}): {e}")
if not isinstance(obj, dict):
raise RulesError(f"Rule record must be a JSON object ({rel}:{i})")
errors = sorted(validator.iter_errors(obj), key=lambda err: list(err.absolute_path))
if errors:
first = errors[0]
path_bits = ".".join([str(p) for p in first.absolute_path]) if first.absolute_path else "<root>"
raise RulesError(f"Rule schema violation ({rel}:{i}) at {path_bits}: {first.message}")
rec = RuleRecord(raw=obj, source_path=rel, line_no=i)
rid = rec.id
if not rid:
raise RulesError(f"Rule missing id ({rel}:{i})")
if rid in seen_ids:
raise RulesError(f"Duplicate rule id '{rid}' in {rel}:{i} (already seen in {seen_ids[rid]})")
seen_ids[rid] = f"{rel}:{i}"
rules.append(rec)
return rules

Some files were not shown because too many files have changed in this diff Show more