V40 — Data Studio : validité, dérive, et un diff auditable - #58
Conversation
La qualité se mesurait en complétude et en cohérence de type. Ce qui manquait, c'est la VALIDITÉ : une valeur peut être présente, bien typée, et pourtant impossible. Cinq règles nommées le disent en clair — âge hors 0-120, date dans le futur, pourcentage hors 0-100, montant négatif, code postal malformé. Puis la cohérence inter-colonnes : une date de fin avant son début, un total qui n'est pas quantité × prix — chaque cellule correcte, la LIGNE impossible. Le choix de moteur du PLAN était mauvais, et le construire l'a montré. Il proposait de faire passer les règles de cohérence par le DuckDB de V29, puisque le fichier y est déjà enregistré. Mais DuckDB est un téléchargement annoncé de 18 à 22 Mo, sur consentement : y router ces contrôles aurait rendu une vérification universelle conditionnelle à un téléchargement que la plupart des utilisateurs refuseront, laissant le panneau vide pour eux. Comparer deux colonnes d'un tableau déjà en mémoire est une boucle : c'est donc une boucle, et tout le monde y a droit. DuckDB garde le travail dont il est réellement indispensable — le SQL arbitraire, et le nouvel export Parquet. Les deux familles de règles obéissent aux deux lois établies par le lecteur de V38 : elles se déclenchent sur preuve et jamais sur le seul nom d'une colonne — une colonne « age » contenant 20 000 est une durée en jours, donc la règle vérifie que l'essentiel de la colonne est plausible avant de signaler le reste, et se tait sinon — et elles signalent sans jamais réparer, parce que la recette de V39 est le seul registre de ce qui a été fait aux données. Trois choses rendent ensuite le studio auditable plutôt que simplement utile : - Un diff avant/après nommant quelles lignes, quelles colonnes et quelles valeurs ont changé. La difficulté : une recette supprime des lignes et ajoute des colonnes, donc applyRecipe renvoie désormais de quelle ligne SOURCE provient chaque ligne survivante — sans quoi le diff apparierait la ligne 7 avec une autre ligne 7 et signalerait un écran de changements qui n'ont jamais eu lieu. - Un profil de référence rejouable, l'idée du manifeste de V22 appliquée aux données : des bornes de bacs et des parts, jamais des lignes, donc le profil d'un fichier de paie décrit la forme de la distribution des salaires et le salaire de personne — c'est ce qui le rend sûr à versionner à côté du code. Il attribue à un nouveau fichier le même PSI, à six décimales, que la comparaison live à deux fichiers de V11. - Un score décomposé en ses parties, chacune avec son poids et ce qu'elle a réellement coûté. Les poids totalisent 105 et non 100, délibérément : la validité a apporté ses 5 points au lieu de les prendre à une partie existante, car redistribuer aurait changé en silence le sens de tous les scores déjà publiés. 502 tests unitaires, 78 e2e. Sur les 36 tests unitaires ajoutés, onze vérifient qu'une règle REFUSE de se déclencher. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKw6oNC8iZ9Kn7q6x4qom4
There was a problem hiding this comment.
🟡 Changes recommended
There are a few concrete correctness/stability issues (DuckDB Parquet temp-file cleanup and profile parsing accepting NaN/Infinity, plus a UI “−0” display bug) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR completes the V40 “Data Studio” wave by adding (1) validity checks (impossible-but-present values), (2) cross-column consistency checks (row-level contradictions), and (3) auditability features: an explicit before/after recipe diff, a replayable drift reference profile, and a score breakdown that explains the final quality score. It also extends the DuckDB engine with a Parquet-export path intended to be “nearly free” once DuckDB is loaded.
Changes:
- Add validity + consistency rule engines (with refusal-to-fire safeguards) and integrate them into the quality report + score.
- Add audit artifacts: recipe diff (row provenance via
survivingRows) and replayable drift “reference profiles”. - Update UI, translations, docs, and tests (unit + e2e) to display the new findings and score breakdown.
File summaries
| File | Description |
|---|---|
| src/locales/fr.json | Adds FR strings for score breakdown + validity/consistency issue cards. |
| src/locales/en.json | Adds EN strings for score breakdown + validity/consistency issue cards. |
| src/features/data/sql/engine.ts | Adds toParquet(sql) export path using DuckDB COPY ... TO. |
| src/features/data/quality/validity.ts | New validity rule engine (age/date/percent/negative/postcode). |
| src/features/data/quality/validity.test.ts | Unit tests for validity rules and refusal-to-fire behavior. |
| src/features/data/quality/v40.test.ts | Tests score breakdown correctness and weight stability (105 total). |
| src/features/data/quality/types.ts | Extends QualityReport with validity/consistency + breakdown fields. |
| src/features/data/quality/reference.ts | New drift reference profile builder/comparator/parser. |
| src/features/data/quality/reference.test.ts | Tests profile safety, parsing, and PSI equivalence with live drift. |
| src/features/data/quality/diff.ts | New recipe diff (explicit source-row alignment). |
| src/features/data/quality/diff.test.ts | Tests diff evidence, alignment after drops, and caps. |
| src/features/data/quality/consistency.ts | New cross-column consistency rules (date order, quantity×price≠total). |
| src/features/data/quality/consistency.test.ts | Unit tests for consistency rules and refusal-to-fire behavior. |
| src/features/data/quality/clean.ts | Returns survivingRows and keeps it aligned through row-dropping steps. |
| src/features/data/quality/checks.ts | Integrates new checks; introduces score breakdown and validity weight. |
| src/features/data/components/QualitySummary.tsx | Displays validity/consistency cards + a score breakdown table. |
| src/features/data/components/DataDropZone.tsx | Adds data-testid for file input (supports e2e). |
| README.md | Documents V40 features and updates test-count claims. |
| PLAN.md | Marks V40 as delivered and updates plan narrative accordingly. |
| e2e/validity.spec.ts | Adds e2e coverage for validity/consistency cards + score breakdown. |
Review details
- Files reviewed: 20/20 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| async toParquet(sql) { | ||
| // A per-call name so two exports can never collide on the virtual FS. | ||
| const name = `export-${Date.now()}.parquet`; | ||
| await connection.query(`COPY (${sql}) TO '${name}' (FORMAT PARQUET, COMPRESSION ZSTD)`); | ||
| const bytes = await db.copyFileToBuffer(name); | ||
| // Registered files live in the Wasm heap: dropping it keeps a session of | ||
| // repeated exports from growing without bound. | ||
| await db.dropFile(name); | ||
| return bytes; | ||
| }, |
| const missing = typeof entry.missingRatio === 'number' ? entry.missingRatio : 0; | ||
| if (entry.kind === 'numeric' && Array.isArray(entry.edges) && Array.isArray(entry.shares)) { | ||
| columns.push({ | ||
| kind: 'numeric', | ||
| column: entry.column, | ||
| missingRatio: missing, | ||
| mean: typeof entry.mean === 'number' ? entry.mean : 0, | ||
| edges: entry.edges.filter((value): value is number => typeof value === 'number'), | ||
| shares: entry.shares.filter((value): value is number => typeof value === 'number'), | ||
| }); | ||
| } else if (entry.kind === 'categorical' && typeof entry.shares === 'object' && entry.shares) { | ||
| const shares: Record<string, number> = {}; | ||
| for (const [name, share] of Object.entries(entry.shares as Record<string, unknown>)) { | ||
| if (typeof share === 'number') shares[name] = share; | ||
| } | ||
| columns.push({ kind: 'categorical', column: entry.column, missingRatio: missing, shares }); | ||
| } |
| {part.ratio !== undefined && ` (${percent(part.ratio)} %)`} | ||
| </td> | ||
| <td className="py-1 pr-3 font-mono text-muted">{part.weight}</td> | ||
| <td className="py-1 font-mono">−{part.penalty}</td> |
V40 — Data Studio : validité, dérive, et un diff auditable
V40 — Data Studio : validité, dérive, et un diff auditable
La qualité se mesurait en complétude et en cohérence de type. Ce qui manquait, c'est la validité : une valeur peut être présente, bien typée, et pourtant impossible.
Deux familles de règles
Validité (une colonne à la fois) — cinq règles nommées en clair : âge hors 0–120, date dans le futur, pourcentage hors 0–100, montant négatif, code postal malformé.
Cohérence (entre colonnes) — une date de fin avant son début, un total qui n'est pas quantité × prix. Chaque cellule correcte, la ligne impossible : la validité ne peut pas le voir.
Le choix de moteur du PLAN était mauvais, et le construire l'a montré
Le PLAN proposait de faire passer les règles de cohérence par le DuckDB de V29, puisque le fichier y est déjà enregistré. Mais DuckDB est un téléchargement annoncé de 18 à 22 Mo, sur consentement. Y router ces contrôles aurait rendu une vérification universelle conditionnelle à un téléchargement que la plupart des utilisateurs refuseront — un panneau qualité vide pour eux.
Comparer deux colonnes d'un tableau déjà en mémoire est une boucle. C'est donc une boucle, et tout le monde y a droit. DuckDB garde le travail dont il est réellement indispensable : le SQL arbitraire, et le nouvel export Parquet (
COPY … TO, un appel, les octets partent directement en téléchargement).Les deux lois héritées du lecteur de V38
agecontenant 20 000 est une durée en jours : la règle vérifie que l'essentiel de la colonne est plausible avant de signaler le reste, et se tait sinon. Sur les 36 tests unitaires ajoutés, onze vérifient qu'une règle refuse de se déclencher — une règle qui signale un bon fichier est pire que pas de règle, parce qu'elle apprend au lecteur à ignorer le panneau.Trois choses qui rendent le studio auditable
Un diff avant/après nommant quelles lignes, quelles colonnes, quelles valeurs ont changé. La difficulté : une recette supprime des lignes et ajoute des colonnes, donc
applyReciperenvoie désormais de quelle ligne source provient chaque ligne survivante. Sans cela, le diff apparierait la ligne 7 avec une autre ligne 7 et signalerait un écran entier de changements qui n'ont jamais eu lieu — un test le fige explicitement.Un profil de référence rejouable, l'idée du manifeste de V22 appliquée aux données : des bornes de bacs et des parts, jamais des lignes. Le profil d'un fichier de paie décrit la forme de la distribution des salaires et le salaire de personne — c'est ce qui le rend sûr à versionner à côté du code. Il attribue à un nouveau fichier le même PSI, à six décimales, que la comparaison live à deux fichiers de V11.
Un score décomposé en ses parties, chacune avec son poids, ce qu'elle a constaté et ce qu'elle a coûté. Les poids totalisent désormais 105 et non 100, délibérément : la validité a apporté ses 5 points au lieu de les prendre à une partie existante, car redistribuer aurait changé en silence le sens de tous les scores déjà publiés. Un fichier sans valeur impossible obtient exactement le score qu'il obtenait avant V40 — un test le vérifie.
Ce que la vague ne fait délibérément pas
Un éditeur de cellules type tableur (les modifications à la main cassent la reproductibilité — la recette est le registre), la déduplication floue (faux positifs garantis sur les noms et adresses, fusionnant silencieusement deux personnes réelles), ni l'imputation par modèle (opaque, et elle fabrique des valeurs d'apparence plausible).
Validation
lint+format:check+tsc --noEmit+ 502 tests unitaires (58 fichiers) + 78 e2e +build— tout vert. Trois e2e ajoutés : un CSV aux valeurs impossibles dont les règles sont nommées, le score décomposé, et un fichier propre qui ne lève aucune carte.Cette vague clôt le groupe Data Studio (V38, V39, V40).
Generated by Claude Code