Commit 52fbc5a
committed
fix(ai): route every report-supplied cell through one markdown encoder
The comparison table interpolated report-provided text straight into
pipe-delimited rows. `fail_reason` is free-form — the suite writes whatever
`classify` produced — and `ai_duel compare` will read any report file it is
handed, so a `|` in one of those fields adds a column to that row and a newline
ends the row early. The table stops being rectangular exactly when a matchup is
already failing, which is the moment the diagnostics are actually read, and the
nightly `cat`s that table into a GitHub issue body.
`md_cell` escapes `|` to `\|` (the documented markdown escape, which renders
correctly and keeps raw CI logs readable) and folds newlines to spaces. It is
applied to every cell that originates in a report rather than in this file:
`matchup_id`, `exercises`, and the reason continuation row. Uniformly, and not
only where a pipe looks reachable today — deciding per field means re-deciding
each time a field is added, and one of those decisions will eventually be wrong.
This one is uncomfortable to write, because the invariant already had a test.
`markdown_rows_are_rectangular` asserted precisely the property that was broken,
and passed — every one of its fixtures was pipe-free, so it verified the renderer
against input that could not trigger the hazard. It was not a weak test of the
right thing; it was a confident test of the wrong thing, and it made the table
look guarded for as long as it existed. `report_supplied_pipes_cannot_add_columns`
is the reachability arm it never had: a matchup id containing a pipe and a
`fail_reason` containing two, asserted to leave the header, separator, data and
reason rows all the same width.
The measurement in that test had to be fixed before it could prove anything. It
first counted `line.split('|')`, which cannot distinguish `\|` from `|` — so it
reported the escaped output as broken, and would equally have reported a broken
encoder as fine had the counts happened to line up. It now counts separators the
way a markdown parser does, skipping any pipe preceded by a backslash. A test that
cannot tell the fix from the defect is not evidence either way.
Evidence. Four mutants, tree restored byte-identical after each, all four killing
`report_supplied_pipes_cannot_add_columns`: making `md_cell` the identity;
reverting the reason call site alone; reverting the matchup-id call site alone;
and keeping newline folding while dropping pipe escaping. The two single-call-site
mutants are the sweep check — a partial revert fails, so the fix is not pinned at
only one of the places it is applied.
Disclosed gap: the `exercises` cell is encoded for uniformity but no test can kill
that call site. It renders `format!("{f:?}")` of a fieldless enum, so no fixture
can put a pipe in it. Reverting that one site alone leaves the suite green. It is
defense against a future `FeatureKind` that carries data, not a defect being
fixed, and it is stated here rather than folded into the mutant count.
Assisted-by: ClaudeCode:claude-opus-51 parent 2ae09a7 commit 52fbc5a
1 file changed
Lines changed: 65 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
688 | 688 | | |
689 | 689 | | |
690 | 690 | | |
691 | | - | |
692 | | - | |
| 691 | + | |
| 692 | + | |
693 | 693 | | |
694 | 694 | | |
695 | 695 | | |
| |||
711 | 711 | | |
712 | 712 | | |
713 | 713 | | |
714 | | - | |
| 714 | + | |
| 715 | + | |
715 | 716 | | |
716 | 717 | | |
717 | 718 | | |
| |||
720 | 721 | | |
721 | 722 | | |
722 | 723 | | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
723 | 739 | | |
724 | 740 | | |
725 | 741 | | |
| |||
1825 | 1841 | | |
1826 | 1842 | | |
1827 | 1843 | | |
| 1844 | + | |
| 1845 | + | |
| 1846 | + | |
| 1847 | + | |
| 1848 | + | |
| 1849 | + | |
| 1850 | + | |
| 1851 | + | |
| 1852 | + | |
| 1853 | + | |
| 1854 | + | |
| 1855 | + | |
| 1856 | + | |
| 1857 | + | |
| 1858 | + | |
| 1859 | + | |
| 1860 | + | |
| 1861 | + | |
| 1862 | + | |
| 1863 | + | |
| 1864 | + | |
| 1865 | + | |
| 1866 | + | |
| 1867 | + | |
| 1868 | + | |
| 1869 | + | |
| 1870 | + | |
| 1871 | + | |
| 1872 | + | |
| 1873 | + | |
| 1874 | + | |
| 1875 | + | |
| 1876 | + | |
| 1877 | + | |
| 1878 | + | |
| 1879 | + | |
| 1880 | + | |
| 1881 | + | |
| 1882 | + | |
| 1883 | + | |
| 1884 | + | |
| 1885 | + | |
| 1886 | + | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
1828 | 1890 | | |
1829 | 1891 | | |
1830 | 1892 | | |
| |||
0 commit comments