Skip to content

fix(md-tables): false COL_MISMATCH + broken-LaTeX recipe in the scanner - #15975

Merged
myia-ai-01 merged 2 commits into
mainfrom
wt/md-tables-currency-colcount
Sep 13, 2026
Merged

myia-ai-01 merged 2 commits into
mainfrom
wt/md-tables-currency-colcount

Conversation

@jsboige

@jsboige jsboige commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

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.py produisait 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_count comptait une colonne de moins (883d1c7d)

_column_count neutralisait chaque match de MATH_SPAN_RE par un simple 'X'. Or un motif $…$ peut ponter le | séparateur de cellules : dans Input ($/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 couvre test_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_count appelle le filler au lieu de sub('X', …).

2. La recette du detail faisait écrire du LaTeX cassé (690e4a5a8)

Le detail d'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çait utiliser \lvert/\rvert.

Deux défauts dans ce libellé :

  1. Aucune règle d'espacement — une commande LaTeX collée à une lettre l'absorbe : \lvert + s = \lverts, commande non définie.
  2. Opérateur manquant — rien n'est nommé pour une condition (le cas réellement visé, π(a|s), p(x|y)), qui demande \mid.

Ce que ça a produit — les deux grains qui ont rencontré cette pathologie :

PR Fichier Écrit par le run Défaut
#15958 Planners-12-LOOP.ipynb $\pi(a\lverts)$ \lverts non défini + \lvert au lieu de \mid
#15965 Search-01-StateSpace.ipynb $\lvertS\lvert$ \lvertS non défini

Rien 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 le detail et 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 de origin/main :

findings
baseline 223
corrigé 217
nouveaux 0

Les 6 disparus sont tous COL_MISMATCH, et exactement la classe visée : 08_Reasoning_Models.ipynb cell 29 L7-9 et 10_LocalLlama.ipynb cell 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-genai du sweep #15719 : 19 → 13 findings ; 08_Reasoning_Models.ipynb et 10_LocalLlama.ipynb passent à 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 sur utiliser \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 ;
  • non-régressions voisines préservées : $|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_state quand 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 690e4a5a8 vient de #15979, fermée pour ne pas déposer deux grains MED/guard consé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

…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>
@github-actions github-actions Bot added the variation-tag-missing PR sans tag Grain: <TIER>/<GENRE> (variation-protocol) label Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Grain tag obligatoire (#10045, bloquant).

Grain tag absent (no Grain: / in body).

Pour passer ce gate, le body doit porter en tete une ligne de la forme :

Grain: <DEEP|MED|LIGHT>/<genre> -- lane <machine:workspace> -- prev: <TIER>/<GENRE> #<PR>

Le <genre> doit figurer dans l'enumeration §1 de variation-protocol.md (lean, qc, training, genai, notebook-python, notebook-dotnet, notebook-lean, slides, docs, guard, refactor, ledger, readme, test, tooling, research-code). Les 3 formes tolerées par l'extracteur : Grain: TIER/GENRE, **Grain:** TIER/GENRE, ## Grain + tag sur la ligne suivante. La lane doit suivre le format <machine>:<workspace> (cf. lane-claim-protocol.md).

…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>
@jsboige jsboige changed the title fix(md-tables): keep the cell delimiter bridged by a currency-unit math span fix(md-tables): false COL_MISMATCH + broken-LaTeX recipe in the scanner Sep 13, 2026
@github-actions github-actions Bot added variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) and removed variation-tag-missing PR sans tag Grain: <TIER>/<GENRE> (variation-protocol) labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-po-2025:CoursIA a deja consomme son budget LIGHT du jour (#15737 (merge a 2026-09-13T02:25:32Z)).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour,
toutes categories LIGHT confondues
(guard, doc, refs, ... partagent un seul budget) :
c'est un RATIO, pas un plafond plat. La decision de merge reste au coordinateur.

@github-actions github-actions Bot added the trivial-diff-advisory Diff trivial : grain META mecanique sans fournee ni exception ecrite (#15740) label Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Trivial-diff advisory (#15740, non bloquant).
genre guard dans la famille META (docs/guard/ledger/readme/test) + diff de 93 lignes changees (<= 100) + aucune exception ecrite dans le body : le litmus de la trivialite (une douzaine d'instances scannees a la suite) est credible. Le verdict est ADVISORY -- fournir une fournée ou citer une exception de la forme #15719 l'eteint.
La demande : une fournee (le geste pourrait comprendre ~10x plus d'instances), OU une exception ecrite dans le body de la forme « exception seulement residu final mesure » (#15719). Editer le body re-deroule cet organe et retire le label.

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 5 et base=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) : \mid pour la condition / \vert pour la valeur absolue, avec la règle d'espacement et sa raison. Le docstring est un """ non-raw ⇒ \\mid en source = \mid au rendu, pas de double backslash livré.
  • l. 548 (le detail du finding MATH_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|rvert sur 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 (python3 absent). 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)

@jsboige

jsboige commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner Author

Path-collision (organ #13359/#13615)

Cette PR #15975 (fix(md-tables): false COL_MISMATCH + broken-LaTeX recipe in the scanner) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur main. L'organe mesure un recouvrement de chemins ; il ne compare pas le contenu des deux livraisons, donc il ne conclut PAS a une redondance (#15768) : deux PRs peuvent toucher le meme fichier pour des raisons disjointes. L'arbitrage reste a la lane ou au coordinateur.

@myia-ai-01
myia-ai-01 merged commit 9197b6d into main Sep 13, 2026
18 of 20 checks passed
jsboige added a commit that referenced this pull request Sep 13, 2026
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>
myia-ai-01 pushed a commit that referenced this pull request Sep 14, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trivial-diff-advisory Diff trivial : grain META mecanique sans fournee ni exception ecrite (#15740) variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants