Files
cs249r_book/book/tools/audit/checks/binary_units.py
Vijay Janapa Reddi 5b6138b697 pass 15 phase A: audit-fix-verify infrastructure
Adds book/tools/audit/ — a five-stage editorial audit pipeline
(SCAN -> PLAN -> FIX -> VERIFY -> REPORT) for the MIT Press round 1
style rules in book-prose-merged.md. Implements Phase A of the plan
at /Users/VJ/Desktop/MIT_Press_Feedback/15_audit_loop/PLAN.md.

Architecture. Five-stage cycle with hard stopping conditions and
verification gating. Five safety checks before any file is written:
no null bytes, no leftover sentinels, expected byte delta matches
actual, quarto structural delta is zero, no new issues introduced.
The byte-delta check alone would have caught the discarded bulk-edit
run that prompted Pass 15.

Files. protected_contexts.py is the load-bearing safety layer with
LineWalker and inline_protected_spans (14 adversarial tests covering
every failure mode from the discarded bulk run). ledger.py is the
JSON schema. scan.py, fix_script_lane.py, verify.py, loop.py are the
stage runners. Seven check modules cover vs_period, compound_prefix,
percent_symbol, lowercase_prose_references, acknowledgements_spelling,
binary_units, and h3_titlecase.

Validation. h3_titlecase finds 609 in vol1 (within 2 of plan's
expected 611). vs_period finds 0 in vol1 (matching pass 10b) and 16
in vol2 (real residuals). compound_prefix finds 19 in vol1 (real
misses pass 10b's regex did not catch) and 46 in vol2 (never swept).
2026-04-07 14:38:00 -04:00

84 lines
2.6 KiB
Python

"""Check: binary units (GiB, TiB, KiB, MiB) in body prose.
Rule: book-prose-merged.md section 1
"Never binary units in prose. Write `GB` and `TB`, not `GiB` or `TiB`."
Auto-fixable: NO. The fix depends on whether the source value was a binary
or decimal quantity. A GiB -> GB conversion changes the number by ~7%. The
correct fix requires either re-reading the spec source or leaving the
value as-is and just changing the unit label (which produces a slightly
incorrect number). We emit as 'needs_subagent=False, auto_fixable=False'
so the operator has to hand-review each case.
Protected contexts: code fences, YAML, math, HTML comments, inline code,
and tables (tables sometimes legitimately show GiB in spec-citation rows).
"""
from __future__ import annotations
import re
from pathlib import Path
from audit.ledger import Issue, make_issue_id
from audit.protected_contexts import (
LineWalker,
default_line_skip,
inline_protected_spans,
is_table_row,
position_in_spans,
)
CATEGORY = "binary-units-in-prose"
RULE = "book-prose-merged.md section 1"
RULE_TEXT = "Never use binary units (GiB, TiB, KiB, MiB) in body prose"
# Match KiB/MiB/GiB/TiB/PiB/EiB when preceded by a digit, possibly with a space
_BINARY_UNIT_RE = re.compile(r"\b\d+(?:\.\d+)?\s*([KMGTPE]iB)\b")
def check(
file_path: Path,
text: str,
scope: str,
start_counter: int = 0,
) -> tuple[list[Issue], int]:
"""Scan for binary unit suffixes in body prose."""
issues: list[Issue] = []
counter = start_counter
walker = LineWalker(text)
for line, state, line_num in walker:
if default_line_skip(line, state):
continue
if is_table_row(line):
continue
if "iB" not in line:
continue
spans = inline_protected_spans(line)
for m in _BINARY_UNIT_RE.finditer(line):
if position_in_spans(m.start(), spans):
continue
unit = m.group(1)
issues.append(
Issue(
id=make_issue_id(scope, CATEGORY, counter),
category=CATEGORY,
rule=RULE,
rule_text=RULE_TEXT,
file=str(file_path),
line=line_num,
col=m.start(),
before=line,
suggested_after="", # No automatic fix; needs review
auto_fixable=False,
needs_subagent=False, # Needs human judgment, not subagent
reason=f"{unit} in body prose (needs manual review)",
)
)
counter += 1
return issues, counter