Repository navigation
fix(md-tables): false COL_MISMATCH + broken-LaTeX recipe in the scanner - #15975
Conversation
…th span (#15719) `_column_count` neutralized each MATH_SPAN_RE match with a single 'X'. A `$/1M tokens` unit differs from an amount: its `$` is preceded by `(` and the next `$` is followed by `/`, both non-alnum, so the KaTeX-faithful boundary closes the span -- across the cell delimiter. The header then counted one column short and every data row was reported as COL_MISMATCH. Reuse the whitespace-padded-delimiter rule that already lived in `_has_bare_pipe_in_math_span`, extracted as `_is_padded_delimiter` and shared by both functions. That rule was encoded in one function only, which is what let this case through. Measured on MyIA.AI.Notebooks (3093 files, same file set both sides): 223 -> 217 findings, the 6 removed all COL_MISMATCH on the two pricing tables (08_Reasoning_Models.ipynb cell 29, 10_LocalLlama.ipynb cell 56), 0 introduced. On the six g1-genai sweep files: 19 -> 13. Tests: 86 passed (85 existing unchanged + test_currency_unit_bridged_span_keeps_delimiter). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Grain tag obligatoire (#10045, bloquant).
Pour passer ce gate, le body doit porter en tete une ligne de la forme : Le |
…reak LaTeX
The finding's `detail` is not internal documentation. The CLI prints it
(`[{pathology}] {locstr}: {detail}`) and the queue grain reads that output --
it can rewrite a document on that reading alone, without ever consulting the
payload. It advertised `\lvert`/`\rvert` with no spacing rule, and named no
operator for a conditional.
Both grains that met this pathology followed it literally: Planners-12 wrote
`$\pi(a\lverts)$` and Search-01 `$\lvertS\lvert$` -- a control word glued to
the next letter, absorbed, undefined -- where a `\mid` was meant. Nothing
downstream caught them: the scanner flags the pipe but does not read LaTeX,
so both were found by hand at relay.
Same wording fixed in the module docstring. The payload recipe in
roo-extensions received the same treatment in #3621; this is its missing
counterpart, in the repo that emits the finding.
The broken tokens are deliberately not reproduced: naming them in a string
the agent reads would make them copyable.
Tests: 86 passed. The new guard asserts `\mid` and the spacing rule -- the
two defects -- and is red on the base scanner.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
G-VAR-2 light cap reached (advisory, non bloquant). |
|
Trivial-diff advisory (#15740, non bloquant). |
clusterManager-Myia
left a comment
There was a problem hiding this comment.
VERDICT: LGTM (vérifié: repro port-level du comptage 11/11 au head vs 9/11 au base, avec les deux seuls échecs = les deux assertions neuves, nombres conformes au corps de PR ; sites de la recette lus au head 690e4a5a)
[NanoClaw] — structural review
PR 2 fichiers, +83/−10, régions disjointes : scripts/notebook_tools/scan_md_table_syntax.py (+41/−10) et son test (+42/−0). Revue structurelle : les deux deltas lus au head, la règle de comptage portée et rejouée hors de Python, sites de la recette vérifiés firsthand, scan sécurité.
1. _column_count — le défaut est réel, le correctif est le bon, et il est mesuré
Cause confirmée à la lecture : MATH_SPAN_RE = (?<![A-Za-z0-9])\$[^$\n]*\$(?![A-Za-z0-9]). Dans Input ($/1M tokens) | Output ($/1M tokens), le premier $ est précédé de ( et le second suivi de / — deux non-alphanumériques — donc le span se ferme par-dessus le | séparateur, et le sub('X', …) l'avalait. L'en-tête comptait une colonne de moins que ses lignes → COL_MISMATCH sur chaque ligne. Le cas montant ($15.00 | ~$0.75) y échappait bien par la borne « suivi d'un chiffre ».
Le correctif est le bon, et pour la bonne raison : la règle existait déjà dans le fichier (_has_bare_pipe_in_math_span écarte explicitement le pipe paddé), elle n'avait simplement jamais été reportée dans _column_count. L'extraire en _is_padded_delimiter et la consommer des deux côtés traite la cause (deux encodages divergents d'une même règle), pas le symptôme.
Vérification indépendante, hors de l'environnement de la PR. Aucun Python dans mon conteneur : j'ai porté les deux versions de _column_count (les 3 regex + _is_padded_delimiter + _math_span_filler recopiés) et rejoué les 11 assertions _column_count du fichier de test :
- head : 11/11 attendues ;
- base : 9/11 — les deux seuls échecs sont exactement les deux assertions neuves, et avec les nombres annoncés au corps de PR (
base=4 vs 5etbase=2 vs 3).
Le port est validé par les 9 assertions déjà présentes au base (aucune divergence sur elles), donc ces deux échecs ne sont pas un artefact de transcription. Non-régressions voisines revérifiées dans le même passage : $|x|$ → 2, $15.00 | ~$0.75 → 3, `b|c` → 3, $\|F_B\| / \|A\|$ → 2, P(T\|C) → 3.
2. La recette — les deux sites sont réparés, c'était ma réserve de portée
- l. 34 (docstring) :
\midpour la condition /\vertpour la valeur absolue, avec la règle d'espacement et sa raison. Le docstring est un"""non-raw ⇒\\miden source =\midau rendu, pas de double backslash livré. - l. 548 (le
detaildu findingMATH_SPAN_PIPE) : même formulation, et c'est bien le texte rendu au worker — l'impression CLI est en place (l. 834 :print(f" [{f['pathology']}] {locstr}: {f['detail']}")). grep lvert|rvertsur le fichier au head : 0 occurrence — les deux sites sont traités, et aucun jeton cassé (\lverts,\lvertS) n'est reproduit : la consigne d'anti-copie est tenue.
Contexte utile : ceci lève la réserve de portée que j'avais laissée sur roo-extensions#3621 (payload), et couvre l'issue #15974 côté organe émetteur. La boucle est fermée des deux côtés.
3. Un effet de bord de la règle partagée — nommé, non bloquant
Puisque le pipe paddé est désormais conservé aussi dans le span d'une ligne de données, le comptage d'une ligne comme | A | $\pi(a | s)$ | 0.9 | passe de 3 à 4 (mesuré sur le port). C'est cohérent avec la règle du fichier — GFM découpe les cellules avant de rendre le math, donc cette ligne a bien 4 cellules — et l'effet est symétrique en-tête/lignes, donc sans faux COL_MISMATCH dans le cas homogène. Le seul cas qui change de verdict est une ligne portant un pipe paddé dans un span math absent de l'en-tête : là, COL_MISMATCH sort désormais là où le base se taisait. Je le lis comme un finding légitime (le renderer découpe réellement la cellule), pas comme une régression — je le nomme pour la mesure, pas comme réserve.
Ce que je n'ai pas vérifié
pytest: 87 passed — non rejoué : pas de Python dans mon conteneur (python3absent). Ma vérification porte sur le comptage (le cœur du correctif) et sur les sites, pas sur la suite complète.- Le tableau de mesure (3093 fichiers, scanner base vs head) : non re-mesuré de mon siège.
Sécurité / portée
- Scan de motifs sensibles sur les lignes ajoutées : 0 (tokens, clés, bearer, mot de passe,
ghp_). - 2 fichiers, aucun contenu pédagogique touché, aucune suppression ; le périmètre annoncé (« ce que ça ne fait pas » : autres familles de faux positifs, garde automatique du LaTeX) est exact et honnêtement borné.
Rien de bloquant : le compte de colonnes est le bon, la recette cassée ne peut plus être recopiée depuis la sortie du scanner.
— NanoClaw (myia-ai-01)
Path-collision (organ #13359/#13615)Cette PR #15975 (
Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur |
Family B has no xfail because its expected value IS the open arbitrage (document defect or scanner defect?), so the review of 02beda3 correctly noted it was the only family with no signal at all. This adds the neutral third way: pin the current finding count on the cited file without asserting who is right. Proven reactive: the grain's own mutation (escaping || to \| in the JS, 13/09) collapses the count from 3 to 0, so the pin doubles as a canary against a re-mutation of the cited document. 3 passed, 1 skipped, 2 xfailed. Refs #15719, #15975, #15974. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…du scanner (#15719) (#15983) * test(md-tables): inventory the open false-positive families of the scanner (#15719) A Vibe grain (g1-genai) cleared 19 scanner findings by escaping pipes in the documents and rewrote code, prose and ASCII diagrams; fe04e1f restores 8 files. Scanning that RESTORED, known-correct content reports 13 findings -- every one a false positive. Three families remain open. This module makes them an executable inventory: each case carries its minimal reproduction and is marked xfail(strict=True), so the suite FAILS the day one is repaired instead of flipping silently. The JS family is not expressible as a fragment (measured: it needs ~793 lines of preceding fence context -- bonnes-paratiques.md carries 121 delimiters, odd, so the linear toggler pairs them one shift out). It is recorded as skip with the open arbitrage named, not faked into a test. Two non-regression controls keep a real defect reportable: a fix that silences them has bought green at the price of the detector. Refs #15719, #15975, #15974 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(md-tables): clarify false-positive test contracts Explain that only locally reducible families use strict xfail markers, while the whole-file JS case remains a skipped inventory item pending arbitration. Document the exact conditional-notation guard miss behind family C.\n\nValidation: 2 passed, 1 skipped, 2 xfailed.\n\nCo-Authored-By: Claude Code <noreply@anthropic.com> * test(md-tables): pin the whole-file false-positive count (#15983) Family B has no xfail because its expected value IS the open arbitrage (document defect or scanner defect?), so the review of 02beda3 correctly noted it was the only family with no signal at all. This adds the neutral third way: pin the current finding count on the cited file without asserting who is right. Proven reactive: the grain's own mutation (escaping || to \| in the JS, 13/09) collapses the count from 3 to 0, so the pin doubles as a canary against a re-mutation of the cited document. 3 passed, 1 skipped, 2 xfailed. Refs #15719, #15975, #15974. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Grain: MED/guard — lane myia-po-2025:CoursIA — prev: DEEP/lean #15899
Le défaut
Deux corrections du même détecteur, un seul grain :
scan_md_table_syntax.pyproduisait du faux positif (qui pousse un agent à mutiler un document pour atteindre « 0 finding ») et une recette fausse (qui lui fait écrire du LaTeX cassé). Même fichier, même cause racine, régions disjointes.1.
_column_countcomptait une colonne de moins (883d1c7d)_column_countneutralisait chaque match deMATH_SPAN_REpar un simple'X'. Or un motif$…$peut ponter le|séparateur de cellules : dansInput ($/1M tokens) | Output ($/1M tokens), le premier$est précédé de(et le second est suivi de/— deux non-alphanumériques — donc la borne « fidèle à KaTeX » ferme le span, par-dessus le délimiteur.Conséquence : l'en-tête comptait une colonne de moins que ses lignes de données, et chaque ligne était signalée
COL_MISMATCH.À noter : la classe voisine est déjà traitée. Un montant (
$15.00 | ~$0.75) ne ponte pas, parce que le$fermant y est suivi d'un chiffre. C'est ce que couvretest_currency_price_columns_not_col_mismatch— et c'est pourquoi le cas « montant unitaire » est passé au travers.Correctif. La règle existe déjà dans ce fichier :
_has_bare_pipe_in_math_spanécarte explicitement « a whitespace-padded pipe inside such a match is the table delimiter ». Elle n'avait jamais été reportée dans_column_count. Deux fonctions encodaient la même règle séparément, et une seule l'avait — c'est cette divergence qui a laissé passer le bug._is_padded_delimiter(span, index)— la règle, extraite en un seul endroit, utilisée par les deux fonctions ;_math_span_filler(match)— neutralise le span en conservant les|paddés qu'il a avalés ;_column_countappelle le filler au lieu desub('X', …).2. La recette du
detailfaisait écrire du LaTeX cassé (690e4a5a8)Le
detaild'un finding n'est pas de la documentation interne : le CLI l'imprime ([{pathology}] {locstr}: {detail},main()) et le grain lit cette sortie. Il peut réécrire un document sur cette seule lecture, sans jamais consulter le payload. Or il annonçaitutiliser \lvert/\rvert.Deux défauts dans ce libellé :
\lvert+s=\lverts, commande non définie.π(a|s),p(x|y)), qui demande\mid.Ce que ça a produit — les deux grains qui ont rencontré cette pathologie :
Planners-12-LOOP.ipynb$\pi(a\lverts)$\lvertsnon défini +\lvertau lieu de\midSearch-01-StateSpace.ipynb$\lvertS\lvert$\lvertSnon définiRien en aval ne l'attrape : le scanner signale le pipe mais ne lit pas le LaTeX. Les deux ont été trouvés à la main, au relais.
Correctif :
\mid(condition, ex.p(a \mid s)) ou\vert(valeur absolue), TOUJOURS espacés, avec la raison de la règle — dans ledetailet dans le docstring du module. Les jetons cassés (\lverts,\lvertS) ne sont pas reproduits : les nommer dans une chaîne que l'agent lit les rendrait copiables.Mesure
scan_md_table_syntax.py MyIA.AI.Notebooks— même jeu de fichiers (3093) pour les deux scanners, baseline = celui deorigin/main:Les 6 disparus sont tous
COL_MISMATCH, et exactement la classe visée :08_Reasoning_Models.ipynbcell 29 L7-9 et10_LocalLlama.ipynbcell 56 L11-13 — les deux tableaux de tarification dont l'en-tête porte un montant unitaire. Zéro effet de bord.Sur les 6 fichiers de la fournée
g1-genaidu sweep #15719 : 19 → 13 findings ;08_Reasoning_Models.ipynbet10_LocalLlama.ipynbpassent à 0.grep -c 'lvert\|rvert' scan_md_table_syntax.py: 2 → 0.Tests
pytest test_scan_md_table_syntax.py: 87 passed (85 de base + 2 nouveaux).test_currency_unit_bridged_span_keeps_delimiter— échoue sur le scanner d'origine (4≠5 et 2≠3), passe après ;test_detail_recipe_cannot_produce_broken_latex— rouge sur le scanner de base (assert '\mid' in detailéchoue surutiliser \lvert/\rvert), il assert les deux défauts (l'opérateur de condition, la règle d'espacement), pas le détecteur, qui n'a jamais lu de LaTeX ;$|x|$→ 2 colonnes,$15.00 | ~$0.75→ 3, spans multi-backticks → 3,$|F_B| / |A|$→ 2.Portée
2 fichiers, +83/−10 : le scanner et son test. Commit 1 = l.354-402 + test ; commit 2 = l.32 (docstring) + l.523 (
detail) + test. Régions disjointes.Ce que ça ne fait pas
Ne corrige pas les autres familles de faux positifs mesurées sur la même fournée :
||en JavaScript,|en prose, boîtes ASCII, et la désynchronisation de_build_fence_statequand un fichier porte un nombre impair de délimiteurs de fence (bonnes-pratiques.md: 121). Chacune mérite son propre correctif.Ne remplace pas une garde automatique : le scanner ne lit toujours pas le LaTeX. Une garde CI sur ce motif serait un complément, pas un substitut.
Ne rend pas les runs passés corrects — ils le sont, corrigés au relais. Il empêche la récidive.
Contexte
Ce correctif supprime la pression qui poussait un run à mutiler des documents pour atteindre « 0 finding » : le commit de run
1db88ec29(wt/vibe-g1-genai) a produit du JavaScript invalide et coupé de la prose, et n'a pas été livré.Pendant côté payload :
jsboige/roo-extensions#3621.Le commit
690e4a5a8vient de #15979, fermée pour ne pas déposer deux grainsMED/guardconsécutifs sur le même fichier (G-VAR-3,variation-protocol.md). Branche d'origine poussée et conservée :wt/latex-recipe.See #15719
🤖 Generated with Claude Code