From 2c2a34b6f9bdf140a808a5e520f0de1614fb501f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 10:23:24 +0000 Subject: [PATCH] docs: README and /docs catch up with V47 to V51 - reference-ml (FR/EN): target and columns locked during a run, one-row classes kept for training, macro averages over present classes as in scikit-learn, damaged share links refused rather than half-rendered - refusals (FR/EN): too-few-rows now also covers a target whose classes all have a single row - limits (FR/EN): three choices set aside (run id, rare-class banner, store slices) and the share-link validator dropped after measurement, its 99-path figure traced to PLAN.md by the existing guard - README: worker lifecycle, share links, sklearn edge case, coverage floor, SHA pins and zizmor, test:coverage script, Cap 7 roadmap Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ELAjHYDyxa5zbbdcke1bMA --- README.md | 31 ++++++++++++++++------ src/content/docs/en/limits.md | 9 +++++++ src/content/docs/en/reference-ml.md | 21 +++++++++++++++ src/content/docs/en/refusals.md | 16 ++++++------ src/content/docs/fr/limites.md | 40 ++++++++++++++++++----------- src/content/docs/fr/reference-ml.md | 24 +++++++++++++++++ src/content/docs/fr/refus.md | 16 ++++++------ src/features/docs/pages.test.ts | 1 + 8 files changed, 119 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b366f7e..7707f8f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ The project follows three non-negotiable principles: are implemented from scratch in TypeScript, seeded end to end — the same seed always reproduces the same run. Core metrics plus compatible ridge and k-NN predictions are checked against committed scikit-learn references generated outside the TypeScript - implementation. + implementation, including the edge where a class is missing from the rows being scored: + macro averages run over the classes present, as scikit-learn computes them. ## Features @@ -78,7 +79,7 @@ The project follows three non-negotiable principles: | No target? | Seeded k-means (k chosen by silhouette) + power-iteration PCA projection, groups described in plain language; date column? **Holt-Winters forecasting** validated by rolling-origin backtest | | MLOps loop | Score a **new batch** with honest test-vs-batch metrics; **compare two runs** side by side with cross-run uncertainty verdicts, or **up to six at once** read against the oldest of the selection; **export a model as JSON and re-import it later** — the exact predictor is rebuilt (byte-identical predictions) and scores any CSV without retraining | | Speed | Heavy families train on **helper cores** (announced on the leaderboard, split by measured cost, never silent), and a model crosses back as JSON so it is rebuilt through the same path an imported model uses. Measured on a 60 000-row run: **74 s → 9.5 s**, with every leaderboard number identical | -| Persistence | Local run history with attached artifacts, opted-in dataset storage (compressed, explicit 50 MB budget), self-contained HTML reports, data-free share links | +| Persistence | Local run history with attached artifacts, opted-in dataset storage (compressed, explicit 50 MB budget), self-contained HTML reports, data-free share links (a damaged link is refused, never half-rendered) | **What the run gives you, past the ranking:** @@ -217,7 +218,12 @@ Wikimedia Commons), the portrait is NASA, public domain._ ## Engineering notes - **Everything off the main thread.** Parsing, cleaning, training, scoring and analysis - run in dedicated Web Workers behind typed message protocols. + run in dedicated Web Workers behind typed message protocols. Every answer a worker can + send has exactly one handler, checked by the compiler, and an answer that arrives for a + state already thrown away is dropped: the target and the features are locked while a + computation that depends on them runs, so a result can never be filed under a question + it did not answer. A worker that crashes is replaced rather than left holding the page, + and the DuckDB engine is closed when its panel goes away. - **From-scratch algorithms**, unit-tested against known results: gradient boosting (quantile bins, second-order gains, Newton leaves), MLP, k-means++, PCA, Holt-Winters, isolation forest, PSI, Shapley values, bootstrap intervals, PR/ROC/calibration curves, @@ -241,16 +247,21 @@ Wikimedia Commons), the portrait is NASA, public domain._ a doc page carries its whole article, in English until the app mounts); Lighthouse mobile ≈ 0.99 on `/ml` under real throttling. Heavy dependencies (Dexie, SheetJS, ONNX Runtime) load lazily. -- **Quality bar.** 923 unit tests and 123 Playwright end-to-end tests across five +- **Quality bar.** 924 unit tests and 123 Playwright end-to-end tests across five projects — desktop, a phone viewport in English and in French, dark mode, and Cloudflare Pages' own routing emulated by `wrangler pages dev` (a real 404, the security headers as served) — covering offline PWA, a fake webcam, a horizontal-overflow guard on every route, and axe-core WCAG A/AA checks on every page including the twenty-four - documentation pages. Plus strict TypeScript, ESLint, Prettier, and Lighthouse budgets — - all enforced in CI. + documentation pages. Plus strict TypeScript, ESLint, Prettier, Lighthouse budgets and + a **coverage floor** set from the measured baseline (statements, branches, functions, + lines), all enforced in CI. - **Supply-chain checks.** Dependabot reviews npm and GitHub Actions weekly, CodeQL analyzes JavaScript and TypeScript on pull requests and `main`, and CI blocks critical - advisories anywhere plus high advisories in dependencies shipped to production. + advisories anywhere plus high advisories in dependencies shipped to production. Every + GitHub Action is pinned to a full commit SHA, no checkout leaves the git token on the + runner, no pull-request value is expanded inside a script, and **zizmor** audits the + workflows on every pull request. A test holds each of those rules per workflow file. + The local language model is fetched at a pinned Hugging Face revision, not `main`. - **One dependency does not come from npm.** SheetJS left the registry, and the copy still published there (`xlsx@0.18.5`) carries two unfixable high advisories. The dependency points at the project's official tarball instead, which fixes both; @@ -277,6 +288,7 @@ npm run dev # start the dev server | Script | Purpose | | --------------------------------------- | -------------------------------------------------------------- | | `npm run test` | Unit tests (Vitest) | +| `npm run test:coverage` | Unit tests with the coverage floor CI enforces | | `npm run e2e` | End-to-end tests (Playwright) | | `npm run typecheck` | TypeScript, strict mode | | `npm run lint` / `npm run format:check` | ESLint / Prettier | @@ -330,7 +342,10 @@ CI builds, tests and deploys on every push: pull requests get a Cloudflare Pages ## Roadmap Development proceeds in planned "caps" of feature waves; six caps have shipped (MVP -through the lab meeting the real world — real photos, real text, real file sizes). The full plan, delivery log and design decisions live in +through the lab meeting the real world: real photos, real text, real file sizes). Cap 7 +makes what already ships hold up under pressure: worker lifecycle, damaged share links, +CI hardening, metric edge cases and a store refactor are delivered; major dependency +upgrades come next. The full plan, delivery log and design decisions live in [PLAN.md](PLAN.md); [CHANGELOG.md](CHANGELOG.md) is extracted from it — one entry per wave, newest first — by `npm run changelog`, and a test fails when the two disagree. diff --git a/src/content/docs/en/limits.md b/src/content/docs/en/limits.md index a0487a6..21e7016 100644 --- a/src/content/docs/en/limits.md +++ b/src/content/docs/en/limits.md @@ -42,6 +42,9 @@ They are not equivalent, and mixing them would be dishonest: | Catching async errors and shipping crash reports | React boundaries cannot see callbacks or event handlers — claiming otherwise would be false; and nothing leaves the browser, telemetry included | | A daily Dependabot and `npm audit fix --force` | A daily cadence drowns the signal; `--force` moves majors without reading the break. The remaining advisories sit in build tooling, and we say so instead of hiding it | | Picking multiclass thresholds automatically | The lab shows its decisions instead of making them for you; and abstention is neither uncertainty calibration nor a conformal guarantee | +| A run id in every worker message | The worker answers in the order it is written to, so the training status is enough to recognise a stale answer. The id becomes necessary the day requests can overlap | +| A « rare class » warning in the interface | A one-row class stays in training, and a target where every class has one row is refused by name; a banner would flag a problem the split already handles | +| Splitting the ML Lab store into independent slices | Every action shares one worker: slicing would have moved code without separating anything | ## Dropped after measurement @@ -65,6 +68,12 @@ person — is covered for 0 MB by an honest refusal. CLIP would answer a arbitrate: the current detector's two misses are inert — both images are already named correctly. +**A field-by-field validator for share links** (V48). Planned. Measured on +three real links with every field deleted, nulled or retyped: the links the +decoder accepted crashed the page on 99 distinct paths. A validator that size +would drift with every new analysis; a guard at render time covers them all, +and a test replays the measurement on every run. + ## Predictions measurement refuted This is the most useful category, and the most uncomfortable. diff --git a/src/content/docs/en/reference-ml.md b/src/content/docs/en/reference-ml.md index f21015d..86e5ef8 100644 --- a/src/content/docs/en/reference-ml.md +++ b/src/content/docs/en/reference-ml.md @@ -31,6 +31,11 @@ triggers task detection (binary, multi-class, regression) and **leak detection**: a column that almost perfectly mirrors the target is excluded automatically and named. +While a run trains, and while anything computed from it is running +(hyperparameter search, learning curve, robust ranking, batch scoring), the +target and the columns are **locked**. Changing them at that moment would file +results under a question they do not answer; wait for the end or cancel. + ## Training and the leaderboard Eight hand-written families, plus an ensemble, plus a naive baseline. Split @@ -43,6 +48,17 @@ that is the price of selection, and hiding it would make the score flattering. The baseline always answers the majority class (or the mean for regression). A model that does not beat it has learned nothing, whatever its absolute score. +The split is stratified by class. A class with **a single row** stays on the +training side: sending it to test would leave the models with no example of it +at all. If every class has a single row, nothing is left to test on, and +training refuses with `too-few-rows` rather than publishing metrics computed +over an empty set. + +Precision, recall and F1 are **macro** averages, computed as scikit-learn does: +over the classes present in the true labels or in the predictions. A class +absent from both does not count as a zero; a class that is predicted but never +true does, because those predictions are all wrong. + ## How solid the numbers are The test set is bootstrap-resampled 1,000 times and the metric recomputed each @@ -123,6 +139,11 @@ rule travels with the export, and the scored CSV then carries two extra columns — see [the formats page](/docs/formats). The five named import refusals are on the [refusals page](/docs/refus). +A share link carries the run's metrics and charts in the URL fragment, never +the data. A cut or damaged link never yields a half-read page: if it does not +decode, or if part of the run it carries does not have the shape LabML writes, +the page refuses to show it and says why. + ## Comparing runs Two runs side by side (`/ml/compare/:a/:b`) or up to six diff --git a/src/content/docs/en/refusals.md b/src/content/docs/en/refusals.md index 45c522b..a473390 100644 --- a/src/content/docs/en/refusals.md +++ b/src/content/docs/en/refusals.md @@ -30,14 +30,14 @@ Two audiences, and the distinction matters: ## ML Lab — training -| Refusal | What triggers it | What to do | -| ------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- | -| `no-features` | Every column was excluded, or none is usable | Re-include at least one column in the columns panel | -| `target-not-found` | The target column is no longer in the file | Pick a target again | -| `task-undetectable` | The target is neither continuous numeric nor usable categorical | Choose another column, or force its type in the Data Studio | -| `too-few-rows` | Grouping without a target needs more rows than there are | Load a larger file | -| `too-few-points` | The time series is too short for an honest forecast | Extend the period, or aggregate less finely | -| `missing-columns` | The file to score lacks columns the model expects | Add the columns named in the message | +| Refusal | What triggers it | What to do | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| `no-features` | Every column was excluded, or none is usable | Re-include at least one column in the columns panel | +| `target-not-found` | The target column is no longer in the file | Pick a target again | +| `task-undetectable` | The target is neither continuous numeric nor usable categorical | Choose another column, or force its type in the Data Studio | +| `too-few-rows` | Grouping without a target needs more rows than there are, or every class of the target has a single row and nothing is left to test on | Load a larger file, or pick a target whose classes repeat | +| `too-few-points` | The time series is too short for an honest forecast | Extend the period, or aggregate less finely | +| `missing-columns` | The file to score lacks columns the model expects | Add the columns named in the message | ### The announced splits diff --git a/src/content/docs/fr/limites.md b/src/content/docs/fr/limites.md index 506e9e1..8558f98 100644 --- a/src/content/docs/fr/limites.md +++ b/src/content/docs/fr/limites.md @@ -27,21 +27,24 @@ Elles ne se valent pas, et les mélanger serait malhonnête : ## Ce qui a été écarté par choix -| Écarté | Pourquoi | -| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Un mode AutoML « on s'occupe de tout » | L'exact contraire d'un laboratoire qui montre ses décisions | -| Une dixième famille de modèles | Neuf suffisent ; une dixième n'améliore ni l'honnêteté ni la compréhension | -| Le deep learning tabulaire | Coût élevé, aucun gain à cette échelle, et ce ne serait plus écrit à la main | -| Un éditeur de cellules façon tableur | Les retouches à la main cassent la reproductibilité — la recette est le seul enregistrement | -| La déduplication floue | Faux positifs garantis sur des noms et des adresses : fusionner deux personnes réelles en silence | -| L'imputation par modèle | Opaque, et elle fabrique des valeurs plausibles — refusée deux fois, en V39 et en V40 | -| Cacher les modèles entre exécutions | La clé de cache serait la configuration entière plus les données ; un hit périmé, c'est un classement faux sans avertissement | -| Deviner la locale d'un fichier | Le navigateur n'a aucun rapport avec le fichier qu'on y dépose | -| Rendre l'indicateur d'absence obligatoire | Il ajouterait des colonnes à toute recette existante : imposer n'est pas annoncer | -| Traduire le changelog et prérendre la documentation française | Le changelog est un journal d'ingénierie, pas un contenu éditorial ; sans routes par langue, des alternates hreflang mentiraient | -| Capter les erreurs asynchrones et envoyer des rapports de plantage | Les boundaries React ne voient pas les callbacks ni les gestionnaires d'événements — prétendre le contraire serait faux ; et rien ne quitte le navigateur, télémétrie comprise | -| Un Dependabot quotidien et un `npm audit fix --force` | Une cadence quotidienne noie le signal ; `--force` change des majeures sans lire la rupture. Les advisories restantes touchent les outils de build, et on le dit plutôt que de le masquer | -| Choisir les seuils multiclasses automatiquement | Le laboratoire montre ses décisions au lieu de les prendre à votre place ; et l'abstention n'est ni une calibration d'incertitude ni une garantie conforme | +| Écarté | Pourquoi | +| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Un mode AutoML « on s'occupe de tout » | L'exact contraire d'un laboratoire qui montre ses décisions | +| Une dixième famille de modèles | Neuf suffisent ; une dixième n'améliore ni l'honnêteté ni la compréhension | +| Le deep learning tabulaire | Coût élevé, aucun gain à cette échelle, et ce ne serait plus écrit à la main | +| Un éditeur de cellules façon tableur | Les retouches à la main cassent la reproductibilité — la recette est le seul enregistrement | +| La déduplication floue | Faux positifs garantis sur des noms et des adresses : fusionner deux personnes réelles en silence | +| L'imputation par modèle | Opaque, et elle fabrique des valeurs plausibles — refusée deux fois, en V39 et en V40 | +| Cacher les modèles entre exécutions | La clé de cache serait la configuration entière plus les données ; un hit périmé, c'est un classement faux sans avertissement | +| Deviner la locale d'un fichier | Le navigateur n'a aucun rapport avec le fichier qu'on y dépose | +| Rendre l'indicateur d'absence obligatoire | Il ajouterait des colonnes à toute recette existante : imposer n'est pas annoncer | +| Traduire le changelog et prérendre la documentation française | Le changelog est un journal d'ingénierie, pas un contenu éditorial ; sans routes par langue, des alternates hreflang mentiraient | +| Capter les erreurs asynchrones et envoyer des rapports de plantage | Les boundaries React ne voient pas les callbacks ni les gestionnaires d'événements — prétendre le contraire serait faux ; et rien ne quitte le navigateur, télémétrie comprise | +| Un Dependabot quotidien et un `npm audit fix --force` | Une cadence quotidienne noie le signal ; `--force` change des majeures sans lire la rupture. Les advisories restantes touchent les outils de build, et on le dit plutôt que de le masquer | +| Choisir les seuils multiclasses automatiquement | Le laboratoire montre ses décisions au lieu de les prendre à votre place ; et l'abstention n'est ni une calibration d'incertitude ni une garantie conforme | +| Un identifiant de run dans chaque message du worker | Le worker répond dans l'ordre où on lui écrit : l'état de l'entraînement suffit à reconnaître une réponse périmée. L'identifiant deviendra nécessaire le jour où des requêtes pourront se chevaucher | +| Un avertissement « classe rare » dans l'interface | Une classe d'une seule ligne reste à l'entraînement, et une cible où chaque classe n'a qu'une ligne est refusée par son nom ; un bandeau signalerait un problème que la découpe règle déjà | +| Découper le store du ML Lab en tranches indépendantes | Toutes ses actions partagent un seul worker : découper aurait déplacé du code sans rien séparer | ## Ce qui a été abandonné après mesure @@ -66,6 +69,13 @@ répondrait à une **autre** question, et le banc ne peut pas l'arbitrer. l'arbitre : les deux ratés du détecteur actuel sont inertes — les deux images sont déjà nommées correctement. +**Un validateur champ par champ pour les liens de partage** (V48). Prévu par le +plan. La mesure, sur trois vrais liens dont chaque champ a été supprimé, mis à +null ou changé de type : les liens acceptés par le décodeur faisaient planter +la page sur 99 chemins différents. Un validateur de cette taille dériverait à +chaque nouvelle analyse ; un garde-fou au rendu les couvre tous, et un test +rejoue la mesure à chaque exécution. + ## Des prédictions que la mesure a démenties C'est la catégorie la plus utile, et la plus inconfortable. diff --git a/src/content/docs/fr/reference-ml.md b/src/content/docs/fr/reference-ml.md index 18d7a04..a2e8fe4 100644 --- a/src/content/docs/fr/reference-ml.md +++ b/src/content/docs/fr/reference-ml.md @@ -33,6 +33,12 @@ et ses extrêmes. Choisir une cible déclenche la détection de tâche une colonne qui reflète presque parfaitement la cible est exclue automatiquement et nommée. +Pendant un entraînement, et pendant tout calcul qui en dépend (recherche +d'hyperparamètres, courbe d'apprentissage, classement robuste, scoring d'un +lot), la cible et les colonnes sont **verrouillées**. Les changer à ce +moment-là classerait les résultats sous une question à laquelle ils ne +répondent pas ; il faut attendre la fin ou annuler. + ## Entraînement et classement Huit familles écrites à la main, plus un ensemble, plus une baseline naïve. @@ -46,6 +52,18 @@ La baseline répond toujours la classe majoritaire (ou la moyenne en régression). Un modèle qui ne la dépasse pas n'a rien appris, quel que soit son score absolu. +La découpe est stratifiée par classe. Une classe qui n'a **qu'une seule +ligne** reste du côté entraînement : l'envoyer au test laisserait les modèles +sans aucun exemple d'elle. Si toutes les classes n'ont qu'une ligne, il ne +reste rien à tester, et l'entraînement refuse avec `too-few-rows` plutôt que +de publier des métriques calculées sur un ensemble vide. + +Précision, rappel et F1 sont des moyennes **macro**, calculées comme dans +scikit-learn : sur les classes présentes dans les vraies valeurs ou dans les +prédictions. Une classe absente des deux ne compte pas comme un zéro ; une +classe prédite mais jamais vraie compte, parce que ces prédictions sont toutes +fausses. + ## Solidité des chiffres Le jeu de test est rééchantillonné 1 000 fois par bootstrap et la métrique @@ -132,6 +150,12 @@ multiclasse figée voyage avec l'export, et le CSV scoré porte alors deux colonnes de plus — voir [la page des formats](/docs/formats). Les cinq raisons de refus à l'import sont nommées — voir la [page des refus](/docs/refus). +Un lien de partage transporte les métriques et les graphiques du run dans le +fragment d'URL, jamais les données. Un lien coupé ou abîmé ne donne jamais une +page à moitié lue : s'il ne se décode pas, ou si une partie du run qu'il porte +n'a pas la forme qu'écrit LabML, la page refuse de l'afficher et dit +pourquoi. + ## Comparer des runs Deux runs côte à côte (`/ml/compare/:a/:b`) ou jusqu'à six diff --git a/src/content/docs/fr/refus.md b/src/content/docs/fr/refus.md index 3607897..6493b61 100644 --- a/src/content/docs/fr/refus.md +++ b/src/content/docs/fr/refus.md @@ -30,14 +30,14 @@ Deux publics, et la distinction compte : ## ML Lab — l'entraînement -| Refus | Ce qui le déclenche | Quoi faire | -| ------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------- | -| `no-features` | Toutes les colonnes ont été exclues, ou aucune n'est utilisable | Réincluez au moins une colonne dans le panneau des colonnes | -| `target-not-found` | La colonne cible n'existe plus dans le fichier | Rechoisissez une cible | -| `task-undetectable` | La cible n'est ni numérique continue ni catégorielle exploitable | Choisissez une autre colonne, ou forcez son type dans le Data Studio | -| `too-few-rows` | Le regroupement sans cible demande plus de lignes qu'il n'y en a | Chargez un fichier plus grand | -| `too-few-points` | La série temporelle est trop courte pour une prévision honnête | Étendez la période, ou agrégez moins finement | -| `missing-columns` | Le fichier à scorer n'a pas les colonnes que le modèle attend | Ajoutez les colonnes nommées dans le message | +| Refus | Ce qui le déclenche | Quoi faire | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `no-features` | Toutes les colonnes ont été exclues, ou aucune n'est utilisable | Réincluez au moins une colonne dans le panneau des colonnes | +| `target-not-found` | La colonne cible n'existe plus dans le fichier | Rechoisissez une cible | +| `task-undetectable` | La cible n'est ni numérique continue ni catégorielle exploitable | Choisissez une autre colonne, ou forcez son type dans le Data Studio | +| `too-few-rows` | Le regroupement sans cible demande plus de lignes qu'il n'y en a, ou chaque classe de la cible n'a qu'une ligne et il ne reste rien à tester | Chargez un fichier plus grand, ou choisissez une cible dont les classes se répètent | +| `too-few-points` | La série temporelle est trop courte pour une prévision honnête | Étendez la période, ou agrégez moins finement | +| `missing-columns` | Le fichier à scorer n'a pas les colonnes que le modèle attend | Ajoutez les colonnes nommées dans le message | ### Les découpes annoncées diff --git a/src/features/docs/pages.test.ts b/src/features/docs/pages.test.ts index 2d313b4..db779de 100644 --- a/src/features/docs/pages.test.ts +++ b/src/features/docs/pages.test.ts @@ -92,6 +92,7 @@ describe('the limits page', () => { { claim: '35', inPlan: /YOLOX-S \(~35 MB\)/ }, { claim: '0.792', inPlan: /the real champion figure is \*\*0\.792\*\*/ }, { claim: '18.5', inPlan: /they weigh \*\*18\.5\*\*/ }, + { claim: '99', inPlan: /on 99 distinct paths/ }, ]; it.each(TRACEABLE)('traces « $claim » back to PLAN.md', ({ inPlan }) => {