fmt(x / MILLION, suffix='M') -> fmt_count(x, scale='M', precision=...): strip
the division and declare the scale once (fmt_count divides by the same factor
and guards counts >= 0). Only the clean case migrates — the divisor magnitude
must match the glyph, so output is unchanged; 41 sites across 10 chapters pass
the byte-identical gate.
Generalize run_percent_lane's gate into a reusable lane_process(scan_fn,
fn_name, dup_codes) + _ensure_import(fn_name); run_scale_lane reuses it. The
pre-scaled / lowercase-'k' / spaced-glyph / fmt_int sites are queued for a
source-level decision (keep the raw count) rather than re-multiplied.
fmt_int(x, suffix='%'|' percent') -> fmt_percent(round(x)/100, precision=0,
commas=True, style=...): rounding first reproduces the integer percent
byte-for-byte (round(x)/100*100 == round(x)), so these flow through the same
byte-identical gate as the fmt('%') lane. 64 fmt_int sites across 17 chapters.
Adjudicate the final straggler (model_serving p_post_str, prefix='~'): relocate
the "~" approximation marker into the table prose and migrate the formatter to
match its sibling cluster — composite "~1%" verified byte-identical to HEAD.
scan_percent now reports 0 auto-rewritable and 0 queued: the percent value-kind
is fully migrated corpus-wide.
scan_percent emitted edits for multiline fmt(...) calls that the single-line
applier could never splice, so they were silently left as fmt(suffix=' percent')
while their cell-mates migrated. Now multiline calls are routed to the queue
(consistent with the multiplier lane) + a regression test. Hand-migrate the 3
multiline sites in appendix_assumptions (overhead_total x2, overhead_useful):
strip the *100, fmt_percent(..., style='prose'). Verified byte-identical to HEAD
(0/275 value diffs, 0 prose diffs).
Move fmt(x, suffix='%'|' percent') into the typed fmt_percent formatter via a
strict byte-identical gate (run_percent_lane.py): every exported _str AND every
visible-prose preview must match before/after, else the file is reverted
untouched. ~520 sites now flow through fmt_percent's max_ratio guard with zero
change to rendered output — the "10,000%" class of bug can no longer render.
The rewriter is value-honest: fmt(ratio*100, '%') -> fmt_percent(ratio) (drops
the redundant *100), standalone fmt(foo_pct, '%') -> fmt_percent(foo_pct/100).
5 chapters intentionally NOT migrated (gate reverted, surfaced for adjudication):
- data_selection / ml_systems / model_serving: values are -81.8% / 320% / 300%
— the guard correctly refused; these need max_ratio= or reclassification.
- responsible_engr / appendix_inference: need fmt_percent added to imports.
Two cells can export the same bare attr (e.g. ratio_str) where only one is
auto-rewritten and the other (fmt_int) is queued. The bare-name prose patch
wrongly appended $\times$ to the non-rewritten one (60×->60××). Track
mult_vars as Class.attr and match prose refs on the full qualified name
(bare only for module-level exports). Regression test reproduces the
data_engineering case.
Gate feedback from the corpus run:
- inject commas=True when the original fmt() omits it (fmt defaults True,
fmt_multiple defaults False) so grouping like '5,000×' is preserved;
- restrict the auto lane to the EXACT '×' glyph; route literal 'x'/'X' and
space-padded ' ×' suffixes to a 'multiplier-variant' queue (ambiguous:
'x' may be a variable/axis, spacing differs);
- decline any call carrying prefix= (fmt_multiple has none).
4 new regression tests (13 total).
codemod_fmt.py with two strictly separated lanes:
- PROVABLE (auto): fmt(...,suffix='×') -> fmt_multiple(...) in the cell +
$\times$ after each prose ref. fmt and fmt_multiple share the precision
guard, so the rendered magnitude is byte-identical; only the glyph moves
(Regime 2). Prose patch is position-based so repeated refs on one line
(tables, multi-factor sentences) each get exactly one glyph.
- QUEUE (never auto-touched): suffix='%' (fmt_percent needs a 0-1 ratio,
source scale unknowable), K/M/B scale, multi-line, and fmt_int multipliers
(fmt_int rounds; fmt_multiple guards against it) -> JSON queue with the
decision each needs.
Default dry-run; --write applies. Proven end-to-end on training.qmd: 21 cell
+ 26 prose edits, values change (× dropped) but visible prose IDENTICAL via
assess_equiv, prose-contract clean, 61 ambiguous sites queued. 9 unit tests.