Initial iftypeset pipeline
This commit is contained in:
commit
626779d4aa
112 changed files with 9550 additions and 0 deletions
25
.forgejo/workflows/ci.yml
Normal file
25
.forgejo/workflows/ci.yml
Normal 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
12
.gitignore
vendored
Normal 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
41
README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# iftypeset (pubstyle) — publication-quality typesetting pipeline
|
||||
|
||||
This project is a **thin, deterministic runtime** for turning Markdown into high‑quality HTML/PDF using:
|
||||
|
||||
- **A machine‑readable rule registry** (Chicago / Bringhurst pointers; paraphrased rules only)
|
||||
- **Typeset profiles** (`spec/profiles/*.yaml`) that map typographic intent → render tokens
|
||||
- **Post‑render 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
78
STATUS.md
Normal 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 (don’t 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
93
app/ARCHITECTURE.md
Normal 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
130
app/CLI_SPEC.md
Normal 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`
|
||||
98
docs/01-demo-acceptance.md
Normal file
98
docs/01-demo-acceptance.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# iftypeset v0.1 — Demo Acceptance Pack
|
||||
|
||||
This doc defines what “ship‑ready” 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 doesn’t 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** — 3–5 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).
|
||||
|
||||
85
docs/02-competitor-matrix.md
Normal file
85
docs/02-competitor-matrix.md
Normal 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 we’re building
|
||||
|
||||
Not “Markdown to PDF” (that’s 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 don’t 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) | ✓ | ~ | ~ | ~ | — | — | — |
|
||||
|
||||
## What’s 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.”
|
||||
|
||||
138
docs/03-rule-ingestion-sop.md
Normal file
138
docs/03-rule-ingestion-sop.md
Normal 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”.
|
||||
|
||||
## Non‑negotiables (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 you’re 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 can’t 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:
|
||||
|
||||
- 1–3 `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). Don’t.
|
||||
- **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.
|
||||
|
||||
126
docs/04-renderer-strategy.md
Normal file
126
docs/04-renderer-strategy.md
Normal 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 can’t be measured with an engine, report it explicitly (don’t 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 can’t 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 don’t need pixel-perfect equivalence; we need “quality gates still pass”.
|
||||
|
||||
236
docs/05-external-evaluation-prompt.md
Normal file
236
docs/05-external-evaluation-prompt.md
Normal 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 **machine‑readable 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.
|
||||
- **Post‑render QA gates** that can fail builds when layout degrades (widows/orphans/keeps/overflow/link-wrap/numbering issues).
|
||||
|
||||
### Non‑negotiables (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`: 1–2 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`: YYYY‑MM or `unknown`
|
||||
- `response_date_utc`: ISO 8601
|
||||
- `web_access_used`: `yes|no`
|
||||
|
||||
## 4) Evaluation rubric (scorecard)
|
||||
|
||||
Score each category 0–5 and write 1–3 sentences of justification.
|
||||
|
||||
### 4.1 Product + positioning
|
||||
|
||||
1) **Problem clarity (0–5)**
|
||||
Does this solve a real pain for teams shipping PDFs, beyond “another renderer”?
|
||||
|
||||
2) **Differentiation (0–5)**
|
||||
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 (0–5)**
|
||||
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 (0–5)**
|
||||
Can a new contributor follow `README.md` and get a useful output quickly?
|
||||
|
||||
12) **Spec readability (0–5)**
|
||||
Are `spec/manifest.yaml`, `spec/profiles/*.yaml`, and `spec/quality_gates.yaml` self-explanatory enough for a reviewer?
|
||||
|
||||
13) **Market-facing clarity (0–5)**
|
||||
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 (0–5)**
|
||||
Are `rule.schema.json`, `manifest.yaml`, and the profile/gate model coherent and extensible?
|
||||
|
||||
5) **Enforcement model (0–5)**
|
||||
Is the split between `lint` / `typeset` / `postrender` / `manual` realistic? Are “manual checklist” rules handled honestly?
|
||||
|
||||
6) **Determinism strategy (0–5)**
|
||||
Does the repo clearly define what “deterministic” means (inputs, renderer versions, fonts, outputs)?
|
||||
|
||||
### 4.3 Rules + content quality
|
||||
|
||||
7) **Rule record quality (0–5)**
|
||||
Do rule records look like paraphrases with pointers (not copied text)? Are IDs/tags/keywords useful?
|
||||
|
||||
8) **Coverage strategy (0–5)**
|
||||
Are we prioritizing the right categories first (numbers/punctuation/citations/layout), and is coverage reporting useful?
|
||||
|
||||
### 4.4 UX / operational usability
|
||||
|
||||
9) **CLI ergonomics (0–5)**
|
||||
Is the CLI spec clear for CI usage (exit codes, JSON artifacts, strictness flags)?
|
||||
|
||||
10) **Integration story (0–5)**
|
||||
Is Forgejo integration plausible and incremental (CSS first, then QA gates)?
|
||||
|
||||
### 4.5 Market viability (compare to existing options)
|
||||
|
||||
Rate each 0–5 based on *your experience* (no need to be exhaustive; avoid vendor hype).
|
||||
|
||||
14) **Replace vs complement (0–5)**
|
||||
Is `iftypeset` best positioned as a replacement for existing toolchains, or as a QA layer you plug into them?
|
||||
|
||||
15) **Who pays first (0–5)**
|
||||
Does the repo make it clear who would adopt/pay first (docs teams, GRC, legal, research, vendors)?
|
||||
|
||||
16) **Defensible wedge (0–5)**
|
||||
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 one‑line 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 **0–5** and include 1–2 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 5–15 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 (5–10 bullets)**
|
||||
2) **Scorecard (0–5 each)**
|
||||
3) **Fundamental flaw checklist (PASS/RISK/FAIL)**
|
||||
4) **Top risks (P0/P1)**
|
||||
5) **Patch suggestions (with diffs if possible)**
|
||||
6) **Go / No‑Go 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)?
|
||||
73
docs/06-project-overview.md
Normal file
73
docs/06-project-overview.md
Normal 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. It’s 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 don’t drift)
|
||||
|
||||
- **Copying book text into the repo:** we can use OCR to locate pointers, but we must not persist verbatim passages.
|
||||
- **Pretending manual rules don’t exist:** if it can’t 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 can’t measure it reliably, it’s a “should” or “manual”, not a “must”.
|
||||
|
||||
## Why this is valuable
|
||||
|
||||
The differentiator is not “Markdown to PDF”. It’s:
|
||||
|
||||
**A) auditable rules** (paraphrase + pointer discipline) and
|
||||
**B) enforceable layout QA** (fail the build when it’s sloppy).
|
||||
|
||||
That’s 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).
|
||||
|
||||
40
docs/07-session-resilience.md
Normal file
40
docs/07-session-resilience.md
Normal 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, you’re 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 what’s next.
|
||||
- Don’t rely on chat logs for state; copy any critical decisions into `docs/`.
|
||||
17
docs/CHECKPOINTS.md
Normal file
17
docs/CHECKPOINTS.md
Normal 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
|
||||
|
||||
3
fixtures/abbreviations.md
Normal file
3
fixtures/abbreviations.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Abbreviations
|
||||
|
||||
Define abbreviations on first use, for example "Minimal Viable Product (MVP)".
|
||||
3
fixtures/accessibility_alt.md
Normal file
3
fixtures/accessibility_alt.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Accessibility
|
||||
|
||||

|
||||
5
fixtures/backmatter_stub.md
Normal file
5
fixtures/backmatter_stub.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Back Matter
|
||||
|
||||
## Appendix A
|
||||
|
||||
Supplemental material goes in appendices.
|
||||
4
fixtures/citations_author_date.md
Normal file
4
fixtures/citations_author_date.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Author-Date
|
||||
|
||||
Use author-date citations in contexts where quick lookup is needed, such as
|
||||
Smith 2024, 15-18.
|
||||
4
fixtures/citations_bibliography.md
Normal file
4
fixtures/citations_bibliography.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Bibliography
|
||||
|
||||
List sources in a consistent order, and keep formatting consistent across
|
||||
entries.
|
||||
4
fixtures/citations_notes.md
Normal file
4
fixtures/citations_notes.md
Normal 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
3
fixtures/code_inline.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Inline Code
|
||||
|
||||
Use `inline_code()` for identifiers and small code tokens.
|
||||
5
fixtures/code_long_lines.md
Normal file
5
fixtures/code_long_lines.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Long Code Lines
|
||||
|
||||
```text
|
||||
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
|
||||
```
|
||||
3
fixtures/figures_placeholder.md
Normal file
3
fixtures/figures_placeholder.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Figures
|
||||
|
||||
Figure 1 shows a placeholder description for figure handling.
|
||||
5
fixtures/frontmatter_stub.md
Normal file
5
fixtures/frontmatter_stub.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Front Matter
|
||||
|
||||
## Preface
|
||||
|
||||
Introductory material lives here.
|
||||
9
fixtures/headings_basic.md
Normal file
9
fixtures/headings_basic.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Headings Overview
|
||||
|
||||
## Subsection One
|
||||
|
||||
Content under the first subsection.
|
||||
|
||||
## Subsection Two
|
||||
|
||||
Content under the second subsection.
|
||||
5
fixtures/headings_deep.md
Normal file
5
fixtures/headings_deep.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Main Title
|
||||
|
||||
### Skipped Heading Level
|
||||
|
||||
This heading skips a level to test lint.
|
||||
7
fixtures/headings_numbered.md
Normal file
7
fixtures/headings_numbered.md
Normal 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
3
fixtures/hyphenation.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Hyphenation
|
||||
|
||||
Use hyphenation only when it improves clarity in compound modifiers.
|
||||
3
fixtures/i18n_quotes.md
Normal file
3
fixtures/i18n_quotes.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# International Quotes
|
||||
|
||||
Use locale-appropriate quotation styles when writing in non-English contexts.
|
||||
5
fixtures/layout_pagebreaks.md
Normal file
5
fixtures/layout_pagebreaks.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Page Breaks
|
||||
|
||||
## Section One
|
||||
|
||||
Content that should stay with the heading.
|
||||
6
fixtures/layout_spacing.md
Normal file
6
fixtures/layout_spacing.md
Normal 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
5
fixtures/layout_widow.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Widow Risk
|
||||
|
||||
Short line.
|
||||
|
||||
Another short line.
|
||||
3
fixtures/links_bare_url.md
Normal file
3
fixtures/links_bare_url.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Bare URL
|
||||
|
||||
Use a bare URL like http://example.com when you need the literal address.
|
||||
4
fixtures/links_long_url.md
Normal file
4
fixtures/links_long_url.md
Normal 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
|
||||
5
fixtures/lists_ordered.md
Normal file
5
fixtures/lists_ordered.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Ordered List
|
||||
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Third step
|
||||
5
fixtures/lists_unordered.md
Normal file
5
fixtures/lists_unordered.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Unordered List
|
||||
|
||||
- Alpha
|
||||
- Beta
|
||||
- Gamma
|
||||
3
fixtures/numbers_currency.md
Normal file
3
fixtures/numbers_currency.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Currency
|
||||
|
||||
Use consistent currency formatting, such as USD 1,200.00 or $1,200.00.
|
||||
4
fixtures/numbers_dates.md
Normal file
4
fixtures/numbers_dates.md
Normal 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.
|
||||
3
fixtures/numbers_ranges.md
Normal file
3
fixtures/numbers_ranges.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Inclusive Ranges
|
||||
|
||||
Use en dashes for ranges like 12-24 and avoid ambiguous shortening.
|
||||
4
fixtures/numbers_spelling.md
Normal file
4
fixtures/numbers_spelling.md
Normal 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.
|
||||
4
fixtures/punctuation_colons.md
Normal file
4
fixtures/punctuation_colons.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Colons
|
||||
|
||||
Use a colon after a complete clause when introducing a list: for example,
|
||||
items, examples, and exceptions.
|
||||
4
fixtures/punctuation_commas.md
Normal file
4
fixtures/punctuation_commas.md
Normal 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.
|
||||
4
fixtures/punctuation_dashes.md
Normal file
4
fixtures/punctuation_dashes.md
Normal 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.
|
||||
4
fixtures/punctuation_quotes.md
Normal file
4
fixtures/punctuation_quotes.md
Normal 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.
|
||||
4
fixtures/punctuation_semicolons.md
Normal file
4
fixtures/punctuation_semicolons.md
Normal 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
29
fixtures/sample.md
Normal 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")
|
||||
```
|
||||
5
fixtures/tables_alignment.md
Normal file
5
fixtures/tables_alignment.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Table Alignment
|
||||
|
||||
| Left | Center | Right |
|
||||
| :--- | :---: | ---: |
|
||||
| L | C | R |
|
||||
6
fixtures/tables_basic.md
Normal file
6
fixtures/tables_basic.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Basic Table
|
||||
|
||||
| Name | Value |
|
||||
| --- | --- |
|
||||
| Alpha | 10 |
|
||||
| Beta | 20 |
|
||||
5
fixtures/tables_wide.md
Normal file
5
fixtures/tables_wide.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Wide Table
|
||||
|
||||
| C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| A | B | C | D | E | F | G | H |
|
||||
3
fixtures/typography_fonts.md
Normal file
3
fixtures/typography_fonts.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Font Stacks
|
||||
|
||||
Choose serif and sans-serif stacks that match the target medium.
|
||||
117
forgejo/README.md
Normal file
117
forgejo/README.md
Normal 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)
|
||||
|
||||
Forgejo’s `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
16
pyproject.toml
Normal 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
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
PyYAML==6.0.2
|
||||
jsonschema==4.19.2
|
||||
56
scripts/audit.sh
Executable file
56
scripts/audit.sh
Executable 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
65
scripts/checkpoint.sh
Executable 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
26
scripts/ci.sh
Executable 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
88
spec/examples/README.md
Normal 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 (000–999+)
|
||||
|
||||
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:
|
||||
|
||||
* 30–80 fixtures total
|
||||
* each fixture should target 3–10 rules max
|
||||
* include “degraded mode” fixtures (intentionally malformed Markdown)
|
||||
|
||||
112
spec/extraction_plan.md
Normal file
112
spec/extraction_plan.md
Normal 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** (150–250 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
59
spec/house/HOUSE_RULES.md
Normal 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
74
spec/indexes/README.md
Normal 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
329
spec/indexes/category.json
Normal 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"
|
||||
]
|
||||
}
|
||||
317
spec/indexes/enforcement.json
Normal file
317
spec/indexes/enforcement.json
Normal 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"
|
||||
]
|
||||
}
|
||||
1883
spec/indexes/keywords_all.json
Normal file
1883
spec/indexes/keywords_all.json
Normal file
File diff suppressed because it is too large
Load diff
732
spec/indexes/source_refs_all.json
Normal file
732
spec/indexes/source_refs_all.json
Normal 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
142
spec/manifest.yaml
Normal 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)"
|
||||
|
||||
84
spec/profiles/dense_tech.yaml
Normal file
84
spec/profiles/dense_tech.yaml
Normal 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
76
spec/profiles/memo.yaml
Normal 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: ","
|
||||
|
||||
88
spec/profiles/print_pdf.yaml
Normal file
88
spec/profiles/print_pdf.yaml
Normal 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: ","
|
||||
|
||||
76
spec/profiles/slide_deck.yaml
Normal file
76
spec/profiles/slide_deck.yaml
Normal 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: ","
|
||||
|
||||
96
spec/profiles/web_pdf.yaml
Normal file
96
spec/profiles/web_pdf.yaml
Normal 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
132
spec/quality_gates.yaml
Normal 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
|
||||
|
||||
4
spec/rules/accessibility/v1_accessibility_001.ndjson
Normal file
4
spec/rules/accessibility/v1_accessibility_001.ndjson
Normal 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"}
|
||||
16
spec/rules/citations/v1_citations_001.ndjson
Normal file
16
spec/rules/citations/v1_citations_001.ndjson
Normal 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 style’s 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 author’s 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 style’s 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"}
|
||||
45
spec/rules/citations/v1_citations_002.ndjson
Normal file
45
spec/rules/citations/v1_citations_002.ndjson
Normal 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"}
|
||||
4
spec/rules/code/v1_code_001.ndjson
Normal file
4
spec/rules/code/v1_code_001.ndjson
Normal 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"}
|
||||
12
spec/rules/headings/v1_headings_001.ndjson
Normal file
12
spec/rules/headings/v1_headings_001.ndjson
Normal 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 document’s 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 document’s 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 document’s 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"}
|
||||
20
spec/rules/headings/v1_headings_002.ndjson
Normal file
20
spec/rules/headings/v1_headings_002.ndjson
Normal 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"}
|
||||
12
spec/rules/layout/v1_layout_001.ndjson
Normal file
12
spec/rules/layout/v1_layout_001.ndjson
Normal 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 paragraph’s 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"}
|
||||
30
spec/rules/layout/v1_layout_002.ndjson
Normal file
30
spec/rules/layout/v1_layout_002.ndjson
Normal 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"}
|
||||
4
spec/rules/layout/v1_layout_003.ndjson
Normal file
4
spec/rules/layout/v1_layout_003.ndjson
Normal 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 document’s 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":"Don’t 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"}
|
||||
5
spec/rules/links/v1_links_001.ndjson
Normal file
5
spec/rules/links/v1_links_001.ndjson
Normal 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"}
|
||||
12
spec/rules/numbers/v1_numbers_001.ndjson
Normal file
12
spec/rules/numbers/v1_numbers_001.ndjson
Normal 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 (1990’s).","rationale":"Apostrophes signal possession, not pluralization.","enforcement":"lint","autofix":"rewrite","autofix_notes":"Rewrite common decade plural patterns like ‘1990’s’ to ‘1990s’ outside code blocks.","tags":["plurals","decades"],"keywords":["1990s","decade plural","apostrophe"],"dependencies":[],"exceptions":["Possessive forms remain valid when intended (e.g., ‘1990’s 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., 10–15) 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 10–15’.","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"}
|
||||
50
spec/rules/numbers/v1_numbers_002.ndjson
Normal file
50
spec/rules/numbers/v1_numbers_002.ndjson
Normal 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"}
|
||||
15
spec/rules/punctuation/v1_punctuation_001.ndjson
Normal file
15
spec/rules/punctuation/v1_punctuation_001.ndjson
Normal 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 sentence’s 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 digit–digit (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 source’s 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"}
|
||||
40
spec/rules/punctuation/v1_punctuation_002.ndjson
Normal file
40
spec/rules/punctuation/v1_punctuation_002.ndjson
Normal 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"}
|
||||
8
spec/rules/tables/v1_tables_001.ndjson
Normal file
8
spec/rules/tables/v1_tables_001.ndjson
Normal 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"}
|
||||
15
spec/rules/tables/v1_tables_002.ndjson
Normal file
15
spec/rules/tables/v1_tables_002.ndjson
Normal 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"}
|
||||
8
spec/rules/typography/v1_typography_001.ndjson
Normal file
8
spec/rules/typography/v1_typography_001.ndjson
Normal 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 text’s 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"}
|
||||
7
spec/rules/typography/v1_typography_002.ndjson
Normal file
7
spec/rules/typography/v1_typography_002.ndjson
Normal 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":"Don’t 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":"Don’t 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 document’s 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"}
|
||||
181
spec/schema/rule.schema.json
Normal file
181
spec/schema/rule.schema.json
Normal 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 } } }
|
||||
}
|
||||
]
|
||||
}
|
||||
4
src/iftypeset/__init__.py
Normal file
4
src/iftypeset/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
431
src/iftypeset/cli.py
Normal file
431
src/iftypeset/cli.py
Normal 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
242
src/iftypeset/css_gen.py
Normal 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"
|
||||
67
src/iftypeset/index_builder.py
Normal file
67
src/iftypeset/index_builder.py
Normal 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
343
src/iftypeset/linting.py
Normal 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
329
src/iftypeset/md_parser.py
Normal 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
344
src/iftypeset/qa.py
Normal 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
384
src/iftypeset/rendering.py
Normal 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
104
src/iftypeset/reporting.py
Normal 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
78
src/iftypeset/rules.py
Normal 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
Loading…
Add table
Reference in a new issue