From 4ccc642253d4ccbd004b0461188679b25cb507dd Mon Sep 17 00:00:00 2001 From: quill Date: Sat, 31 Oct 2026 13:50:46 +0000 Subject: [PATCH] quill: add md-heading-normalize v1 with passing verification test --- wizard-work/quill/md-heading-normalize.py | 49 +++++++++++++++++++ .../quill/test_md_heading_normalize.py | 39 +++++++++++++++ .../quill/verification-report-2026-10-31.md | 26 ++++++++++ 3 files changed, 114 insertions(+) create mode 100644 wizard-work/quill/md-heading-normalize.py create mode 100644 wizard-work/quill/test_md_heading_normalize.py create mode 100644 wizard-work/quill/verification-report-2026-10-31.md diff --git a/wizard-work/quill/md-heading-normalize.py b/wizard-work/quill/md-heading-normalize.py new file mode 100644 index 0000000..ebb80b4 --- /dev/null +++ b/wizard-work/quill/md-heading-normalize.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Normalize markdown heading styles to consistent ATX format.""" +import re +import sys + +_ATX_RE = re.compile(r'^(#{1,})(\s+)(.+)$') + +def normalize(text: str) -> str: + lines = text.splitlines() + out = [] + i = 0 + while i < len(lines): + line = lines[i] + stripped = line.rstrip() + next_line = lines[i + 1].rstrip() if i + 1 < len(lines) else '' + # Setext h1/h2: only if current line is NOT already an ATX heading + if not _ATX_RE.match(stripped): + if re.fullmatch(r'=+\s*$', next_line): + out.append(f'# {stripped}') + i += 2 + continue + if re.fullmatch(r'-+\s*$', next_line): + out.append(f'## {stripped}') + i += 2 + continue + # ATX heading: cap level at 6 + m = _ATX_RE.match(stripped) + if m: + hashes, space, content = m.group(1), m.group(2), m.group(3) + level = min(len(hashes), 6) + out.append(f'{"#" * level}{space}{content}') + i += 1 + continue + out.append(stripped) + i += 1 + return '\n'.join(out) + ('\n' if text.endswith('\n') else '') + +def main() -> int: + if len(sys.argv) < 2: + print('usage: md-heading-normalize.py ', file=sys.stderr) + return 2 + path = sys.argv[1] + with open(path, 'r', encoding='utf-8') as f: + src = f.read() + sys.stdout.write(normalize(src)) + return 0 + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/wizard-work/quill/test_md_heading_normalize.py b/wizard-work/quill/test_md_heading_normalize.py new file mode 100644 index 0000000..42aafab --- /dev/null +++ b/wizard-work/quill/test_md_heading_normalize.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Verification test: md-heading-normalize produces correct ATX output.""" +import subprocess +import sys +import tempfile +import os + +SCRIPT = '/root/.hermes/wizard-outputs/quill/work/md-heading-normalize.py' + +CASES = [ + # (input, expected_output) + ("Title\n=====\n\nBody.\n", "# Title\n\nBody.\n"), + ("Section\n-------\n", "## Section\n"), + ("# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6\n####### H7\n", + "# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6\n###### H7\n"), + ("plain text\n# not setext\n===\n", "plain text\n# not setext\n===\n"), # only setext if *next* line is === + ("# H1\nsome text\n## H2\n", "# H1\nsome text\n## H2\n"), +] + +passed = 0 +failed = 0 +for idx, (src, expected) in enumerate(CASES): + with tempfile.NamedTemporaryFile('w', suffix='.md', delete=False) as f: + f.write(src) + path = f.name + try: + result = subprocess.run([sys.executable, SCRIPT, path], capture_output=True, text=True) + got = result.stdout + if got == expected: + passed += 1 + else: + failed += 1 + print(f'FAIL case {idx}:\n expected={expected!r}\n got={got!r}', file=sys.stderr) + finally: + os.unlink(path) + +print(f'PASS={passed} FAIL={failed}') +if failed: + sys.exit(1) diff --git a/wizard-work/quill/verification-report-2026-10-31.md b/wizard-work/quill/verification-report-2026-10-31.md new file mode 100644 index 0000000..a8e2fc9 --- /dev/null +++ b/wizard-work/quill/verification-report-2026-10-31.md @@ -0,0 +1,26 @@ +# Verification Report — md-heading-normalize v1 + +## Artifact +- Script: `md-heading-normalize.py` +- Test: `test_md_heading_normalize.py` + +## Test Results +``` +PASS=5 FAIL=0 +``` + +## Cases Covered +1. Setext h1 (`Title` + `=====` → `# Title`) +2. Setext h2 (`Section` + `-------` → `## Section`) +3. ATX cap: `####### H7` → `###### H7` +4. Setext skipped when current line is ATX heading +5. Round-trip preservation of already-normalized ATX headings + +## Command +``` +python3 test_md_heading_normalize.py +``` + +## Observed +- Exit code: 0 +- 5 tests passed, 0 failed