Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ assignees: ""
<!-- Briefly describe the bug -->

## Steps to Reproduce

1. <!-- First step -->
2. <!-- Second step -->
3. <!-- Third step -->
Expand All @@ -24,6 +25,7 @@ assignees: ""
<!-- What actually happens -->

## Environment

- OS: <!-- e.g., macOS 14.2 -->
- Python version: <!-- e.g., 3.11 -->
- Rust version: <!-- e.g., 1.75 -->
Expand Down
3 changes: 3 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<!-- One-line summary of the change -->

## Changes

- **What was changed**
- **Why this change is needed**
- **How it affects the project**
Expand All @@ -16,9 +17,11 @@
- **Manual testing performed**

## Related Issues

- [ ] None

## Checklist

- [ ] Tests pass
- [ ] Code style compliant
- [ ] Documentation updated
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ jobs:
version-file: uv.lock
args: format --check

# Markdown gates, standalone like ruff above (node is preinstalled on ubuntu runners, so no
# setup step). Same two checks the pre-commit hooks run; pins must stay in sync with
# .pre-commit-config.yaml (markdownlint-cli2 rev and the html-validate additional_dependencies).
# File scope: markdownlint-cli2 reads its globs/ignores from .markdownlint-cli2.jsonc;
# html-validate validates ONLY ```html fences inside markdown (see the pre-commit comment for
# why) via the transform in .htmlvalidate.json.
markdown-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: markdownlint-cli2
run: npx -y markdownlint-cli2@0.23.3
- name: html-validate (```html fences in markdown)
run: npx -y -p html-validate@11.16.4 -p html-validate-markdown@6.0.0 html-validate "**/*.md"

python-type:
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 6 additions & 0 deletions .htmlvalidate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": ["html-validate:recommended"],
"transform": {
"^.*\\.md$": "html-validate-markdown"
}
}
51 changes: 51 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// markdownlint-cli2 configuration — the single source of truth for markdown lint scope.
// Both .pre-commit-config.yaml (markdownlint-cli2 hook) and .github/workflows/ci.yml
// (markdown-lint job, `npx markdownlint-cli2`) read THIS file: the globs/ignores below and
// the version pins must stay in sync with those two files.
//
// Scope deliberately includes top-level *.md, docs/, and .github/ templates, and excludes
// machine-generated/internal trees: .git, node_modules, .venv, site (mkdocs output),
// target (Rust build), __pycache__, .ralph (agent state), plans/ + PLAN.md (agent-generated
// planning artifacts), tests/ (fixture data), .tablassert (runtime artifacts).
{
"config": {
"default": true,
// MD013 (line-length) is off because no line-length convention exists (CHANGELOG prose
// runs to 2500+ chars); wrapping 1300 lines of history is churn without value.
// MD033 is off because inline HTML is allowed on purpose (docs/fullmap.md,
// docs/configuration/table.md use <details>/<summary>/<kbd>/...); its well-formedness is
// validated by html-validate instead (.htmlvalidate.json).
"MD013": false,
"MD033": false,
// CHANGELOG legitimately repeats "### Added"/"### Fixed" under each release heading.
"MD024": {
"siblings_only": true
},
// mkdocs admonitions (!!! / ???) require 4-space-indented paragraphs, and multi-paragraph
// admonitions (blank line between them) parse as indented code blocks under plain GFM —
// MD046 cannot distinguish them. The repo's own typography gate (tests/test_docs_style.py)
// additionally bans HTML comments in docs prose, so per-line markdownlint-disable comments
// are not an option either. The repo uses fenced code blocks everywhere else regardless.
"MD046": false,
// Every table must use "| cell | cell |" (single-space padding) — the dominant existing
// style; --fix normalizes the few inconsistent tables to it.
"MD060": {
"style": "compact"
}
},
"customRules": ["./.markdownlint/no-unicode-chars.cjs"],
"globs": ["**/*.md"],
"ignores": [
"**/.git/**",
"**/node_modules/**",
"**/.venv/**",
"**/site/**",
"**/target/**",
"**/__pycache__/**",
".ralph/**",
"plans/**",
"PLAN.md",
"tests/**",
".tablassert/**"
]
}
78 changes: 78 additions & 0 deletions .markdownlint/no-unicode-chars.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Custom markdownlint rule: bans AI-typical unicode punctuation that "sometimes ends up"
// in markdown files and auto-fixes it to ASCII equivalents:
//
// em/en dashes: — (U+2014), – (U+2013) -> -
// arrows: → (U+2192), ⇒ (U+21D2), ⟶ (U+27F6), -> ->
// ➜ (U+279C), ➔ (U+2794), ⇨ (U+21E8) -> ->
// left arrow: ← (U+2190) -> <-
// bullets: • (U+2022), ‣ (U+2023), ◦ (U+25E6) -> -
//
// Curated blocklist, not a blanket unicode ban: legitimate notation (≤, ≥, ×, ≥, °, é, …)
// stays legal. Extend the tables below rather than loosening the rule.
//
// Loaded via "customRules" in .markdownlint-cli2.jsonc, which both the pre-commit hook and
// the CI markdown-lint job use, so the ban is enforced identically everywhere. The fixInfo
// below makes `markdownlint-cli2 --fix` rewrite violations automatically.
//
// NOTE on interplay with tests/test_docs_style.py (the repo's pre-existing typography gate):
// it bans ASCII arrows ("->", "<-") in user-facing docs PROSE (outside code spans), and its
// file list covers README/CHANGELOG/CONTRIBUTING/CODE_OF_CONDUCT/docs. A rule-fixed arrow in
// that prose will therefore turn the pytest gate red by design; the correct follow-up there
// is to rephrase or wrap the arrow in a code span. The two gates are complementary:
// markdownlint = structure + unicode ban repo-wide, pytest gate = stricter prose typography
// in user-facing files only.
"use strict";

const GROUPS = [
{
label: "em/en dash",
replacement: "-",
regex: /[\u2013\u2014]/g
},
{
label: "left arrow",
replacement: "<-",
regex: /\u2190/g
},
{
label: "arrow",
replacement: "->",
regex: /[\u2192\u21d2\u27f6\u279c\u2794\u21e8]/g
},
{
label: "bullet",
replacement: "-",
regex: /[\u2022\u2023\u25e6]/g
}
];

module.exports = {
names: ["no-unicode-dashes-arrows-bullets", "no-unicode-chars"],
description: "Disallowed unicode punctuation (em/en dash, arrow, bullet); use ASCII",
tags: ["style"],
function: (params, onError) => {
for (const [ index, line ] of params.lines.entries()) {
for (const { label, replacement, regex } of GROUPS) {
regex.lastIndex = 0;
let match = null;
while ((match = regex.exec(line)) !== null) {
const column = match.index + 1;
const codePoint =
match[0].codePointAt(0).toString(16).toUpperCase().padStart(4, "0");
onError({
lineNumber: index + 1,
detail:
`Unicode ${label} '${match[0]}' (U+${codePoint})` +
`; use '${replacement}'`,
range: [ column, match[0].length ],
fixInfo: {
editColumn: column,
deleteCount: match[0].length,
insertText: replacement
}
});
}
}
}
}
};
26 changes: 26 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ repos:
- id: ruff-check
args: [--fix]
- id: ruff-format
# markdownlint-cli2 (Markdown structure rules + custom unicode rule) and html-validate
# (HTML inside markdown, ```html fences only) — the whole-repo gates re-run both with the
# same pins in the `markdown-lint` job in .github/workflows/ci.yml; keep the pins in sync.
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.23.3
hooks:
- id: markdownlint-cli2
args: [--fix]
# File scope comes from the globs/ignores in .markdownlint-cli2.jsonc (same policy as
# ruff above: config decides, so new top-level trees cannot escape the gate).
pass_filenames: false
- repo: local
hooks:
- id: cargo-fmt
Expand All @@ -36,6 +47,21 @@ repos:
language: system
files: ^(pyproject\.toml|uv\.lock)$
pass_filenames: false
# No official pre-commit mirror exists for html-validate, hence a local node hook.
# The third-party html-validate-markdown transformer lints ONLY ```html code fences:
# html-validate's built-in markdown transformer does not escape "<" inside inline code
# spans, so prose like `(?<=...)` or `<name>_<version>` (this repo's placeholder
# convention) is parsed as start tags and fails with unsuppressible parse errors.
# Fence-scoped validation has zero false positives and still gates real HTML blocks.
- id: html-validate
name: html-validate
entry: html-validate "**/*.md"
language: node
additional_dependencies:
# Keep in sync with the npx pins in .github/workflows/ci.yml (markdown-lint job).
- html-validate@11.16.4
- html-validate-markdown@6.0.0
pass_filenames: false
- id: pyright
name: pyright
entry: uv run pyright
Expand Down
Loading
Loading