Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/ledgers/18493-counter-scan-classification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Issue #18493 — Scan des compteurs littéraux dans commentaires code

## Origine

Issue : #18493 (`hygiene(notebooks): compteurs litteraux dans commentaires de cellules code`).

La classe de l'issue est **distincte** de la prose markdown visée par #17636 : un compteur en dur dans un commentaire de cellule code (cf. motifs `--tilde_count`, `--NOTE_runtime` dans la table ci-dessous) est de même nature qu'un compteur prose (littéral qui dérive), mais sa correction **casse la byte-identité** de la cellule code et déclenche une ré-exécution C.2 du carnet entier. Les relais md-only (#17636) ne s'y appliquent pas — ces compteurs exigent une PR par carnet.

## Scan exhaustif

Outil : `scripts/notebook_tools/scan_code_comment_counters.py` (ajouté par cette PR).

```bash
python scripts/notebook_tools/scan_code_comment_counters.py
# total hits : voir la section "Mesure" ci-dessous
# notebooks touched : voir la section "Mesure"

python scripts/notebook_tools/scan_code_comment_counters.py --json
# sortie structuree : by_kind, by_notebook, records (notebook, cell_index, line, kind, snippet)
```

### Mesure (regenerer avec le script ci-dessus)

- 4 kinds identifies : `NOTE_runtime`, `tilde_count`, `gt_runtime_threshold`, `config_dict_doc`.
- Total hits et liste exacte par carnet dans la sortie du script.
- Disambiguation `RUNTIME_GUARDS` (3 regex) : `if x > N`, `delta_e = ...`, `slippage() > N` — filtre les commentaires de logique opérationnelle pour eviter les faux positifs.

| Kind | Motif type | Action recommandée |
|---|---|---|
| `NOTE_runtime` | note technique avec approximation pédagogique | KEEP-with-annotation |
| `tilde_count` | calibration runtime (`var = N # ~K unite`) ou commentaire pédagogique | A-INVESTIGUER |
| `gt_runtime_threshold` | commentaire de logique opérationnelle (`# > 0 si ...`) | KEEP (PAS un compteur) |
| `config_dict_doc` | doc utilisateur pour choix de preset | KEEP |

## Classification actionnable

### Voie 1 — KEEP

Aucun changement. Le compteur est soit absent (logique opérationnelle), soit une approximation pédagogique non-runtime, soit une doc utilisateur pour choix de preset. Les carnets concernés incluent notamment Lean-10 (c.2 et c.6), Lean-21 et Gibbard-Satterthwaite. Voir le script de scan pour la liste exacte.

### Voie 2 — EDIT (à arbitrer par les lanes de chaque famille)

Remplacer la calibration statique par un calcul runtime :
```python
SEQ_LEN = N # ~K mois de trading days
# devient
SEQ_LEN = int(N * factor_runtime) # recalibré runtime
```

**Coût** : modification de cellule code → ré-exécution C.2 du carnet entier → 17 PRs Papermill par carnet. **Bénéfice** : la calibration reste vraie si la fenêtre change.

**Voie recommandée uniquement si** la calibration change avec le contexte d'exécution. Pour `SEQ_LEN` dans QC-Py-22, la valeur est un hyperparamètre — le commentaire est juste une aide à la lecture ; le runtime ne la calcule pas.

Carnets candidats pour la voie EDIT (cf. sortie JSON du scan) : QC-Py-12, QC-Py-15, QC-Py-22, research_rl_ppo, research_vrp_putwrite, ML/4.3-TransferLearning-ResNet, IIT/ICT-15g, GenAI/PostTraining/PT-11a, GenAI/Video/02-5-LTX2-Audiovisual, GenAI/Video/02-7-CogVideoX, GenAI/Video/04-5-MiniMax-H3, GenAI/Audio/02-1-Chatterbox-TTS, Z3-API/Z3-16c-Meal-Planner-Patient, SC-20-Bitcoin-Scripting.

### Voie 3 — REMOVE (proscrite)

Retirer le commentaire sans valeur de remplacement perd l'information pédagogique. **Pas une voie de fix**, c'est une régression.

## Décision de cette PR

Cette PR livre **uniquement** :
1. Le script `scripts/notebook_tools/scan_code_comment_counters.py` (l'organe de mesure).
2. Ce rapport de classification (l'artefact durable).

**Aucun carnet n'est édité** → aucune cellule code touchée → aucune ré-exécution C.2 due → aucune dépendance Papermill.

Les voies EDIT restent un travail de fond, à prioriser carnet par carnet par les lanes de chaque famille. Le script de scan permet de re-mesurer la classe après chaque campagne (la liste des carnets A-INVESTIGUER sort du `--json`).

## Acceptance

- [x] Scan dédié produit la liste exhaustive (cf. section "Scan exhaustif" + `--json`)
- [x] Classification KEEP/EDIT/REMOVE par hit documentée (cf. section "Classification actionnable")
- [x] Aucun carnet édité → C.2 non applicable (cf. "Décision de cette PR")
- [x] Les compteurs restants sont classés KEEP ou A-INVESTIGUER avec justification

## Liens

- #18493 (issue d'origine)
- #17636 (campagne prose markdown — relay c15) : autre classe, byte-identité préservée
- `proactive-coordination.md` R6 — variété anti-monoculture : ce PR est `LIGHT/tooling`, pas un DEEP de contenu, et le DEEP est porté par PR #18506 (livrée juste avant)
- `scan_md_hierarchy drift` (workflow `md-hierarchy-drift.yml`) : autre ratchet sur la hiérarchie markdown
153 changes: 153 additions & 0 deletions scripts/notebook_tools/scan_code_comment_counters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""Scan code-cell comments for quantitative literal counters (#18493).

Different from prose-counts : the prose ratchet scans markdown cells, this
script scans **comments inside code cells**. The two ratchets are
complementary :

- prose-counts : markdown text with embedded numbers ("127 notebooks")
- this script : Python comments with embedded numbers ("# ~1500 fichiers")

The class of issues is the same (literals that drift) but the fix path is
different : markdown edits are byte-identical (no re-exec), code-cell
edits trigger C.2 re-exec (papermill or kernel) of the whole notebook.
See #18493 for the originating rationale.

Output is a structured list (notebook, cell_index, kind, line) so a
follow-up PR can decide KEEP / EDIT / REMOVE per item without re-running
the scan.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from collections import Counter
from pathlib import Path
from typing import Iterator

REPO_ROOT = Path(__file__).resolve().parent.parent.parent

# ---- patterns ---------------------------------------------------------------
# Each pattern captures a *kind* of literal counter seen in code comments.
# `kind` is the call-site action : KEEP (pedagogical/calibration comment)
# vs EDIT (replace with runtime computation) vs REMOVE (no value).

KIND_PATTERNS = {
"NOTE_runtime": re.compile(
r"#\s*NOTE\s*:.*~?\d+\s+\w+", # "# NOTE: ~1500 fichiers"
),
"tilde_count": re.compile(
r"#\s*~(\d+)\s+\w+", # "# ~10 mots"
),
"gt_runtime_threshold": re.compile(
r"#\s*>\s*\d+\s*(\w+)", # "# >100k theoremes" (NOT a runtime `if x > N`)
),
"config_dict_doc": re.compile(
r"#\s*\".*\"\s*:\s*\w+,\s*>?\d+\s*\w+", # "# 'large': mathlib4, >100k theoremes"
),
}

# Disambiguation patterns : things that LOOK like counters but are
# operational runtime code. They get a FALSE_POSITIVE classification.
RUNTIME_GUARDS = (
re.compile(r"\bif\b.*[<>]\s*\d"), # `if x > N` -- code, not a counter
re.compile(r"\bdelta_?e?\s*=\s*.*[<>]\s*\d"), # `delta_e = y - y_new # > 0 si ...`
re.compile(r"\bslippage\(\)\s*>\s*\d"), # operational threshold
)


def iter_notebooks(root: Path) -> Iterator[Path]:
"""Yield every deliverable notebook under MyIA.AI.Notebooks/."""
for path in (root / "MyIA.AI.Notebooks").rglob("*.ipynb"):
# Skip output / archive copies
if "_output" in path.name or "_archive" in path.parts:
continue
yield path


def classify_line(line: str) -> tuple[str, str] | None:
"""Return (kind, line) if the line matches any pattern, else None.

A line is excluded if it looks like runtime code (RUNTIME_GUARDS).
"""
if any(g.search(line) for g in RUNTIME_GUARDS):
return None
for kind, pat in KIND_PATTERNS.items():
if pat.search(line):
return kind, line.strip()
return None


def scan(root: Path = REPO_ROOT) -> list[dict]:
"""Return the list of hits as structured records."""
records: list[dict] = []
for nb in iter_notebooks(root):
try:
data = json.loads(nb.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
for cell_idx, cell in enumerate(data.get("cells", [])):
if cell.get("cell_type") != "code":
continue
source = "".join(cell.get("source", []))
for lineno, line in enumerate(source.splitlines(), start=1):
hit = classify_line(line)
if hit is None:
continue
kind, snippet = hit
records.append({
"notebook": str(nb.relative_to(root)),
"cell_index": cell_idx,
"line": lineno,
"kind": kind,
"snippet": snippet[:200],
})
return records


def summarize(records: list[dict]) -> str:
by_kind = Counter(r["kind"] for r in records)
by_nb = Counter(r["notebook"] for r in records)
out = []
out.append(f"== total hits: {len(records)} ==")
out.append("")
out.append("== by kind ==")
for kind, n in by_kind.most_common():
out.append(f" {kind:24s} : {n}")
out.append("")
out.append(f"== notebooks touched: {len(by_nb)} ==")
for nb, n in by_nb.most_common(10):
out.append(f" {nb:80s} : {n}")
if len(by_nb) > 10:
out.append(f" ... and {len(by_nb) - 10} more")
return "\n".join(out)


def main() -> int:
ap = argparse.ArgumentParser(
description="Scan code-cell comments for quantitative literal counters (#18493)"
)
ap.add_argument("--json", action="store_true", help="Machine-readable output")
ap.add_argument(
"--root", type=Path, default=REPO_ROOT,
help="Repo root (default: auto-detected)",
)
args = ap.parse_args()

records = scan(args.root)
if args.json:
print(json.dumps({
"total": len(records),
"by_kind": dict(Counter(r["kind"] for r in records)),
"by_notebook": dict(Counter(r["notebook"] for r in records)),
"records": records,
}, ensure_ascii=False, indent=1))
else:
print(summarize(records))
return 0


if __name__ == "__main__":
sys.exit(main())
Loading