STUD-602 — courbe de vie longitudinale des longs, en Rust (passage standalone) #1664

Merged
hermes-fusion merged 1 commit from hermes/vie-longue-rust into main 2026-10-14 22:45:03 +02:00
Collaborator

Carte t_1bfed611. Porte la courbe de vie LONGITUDINALE sur le backend Rust, en Python inchangé.

Ce qui est livré

  • rust/collecteur/src/vie_longue.rs : Passage/Entree, cumuls(), ajuster(), taux_heures_vues(), points(), format_de(), et la page page().
  • rust/collecteur/tests/vie_longue.rs + tests/fixtures/vie-longue/ (3 vidéos, 3 passages) : 13 tests, dont un qui lance le binaire.
  • main.rs : sous-commande --vie-longue <videos-vues.jsonl> <publications.json>, en tête de main, sans toucher aux autres sous-commandes.

La forme réelle du fichier (le mandat la supposait autre)

Le mandat annonçait {"date": "...", "videos": [{"id", "vues"}]}. Mesuré sur les copies du 132 disponibles le 2026-10-14 (/home/atelier/tmp-relecteur/*/videos-vues.jsonl, et l'écriture de VUES_VIDEOS dans tableau-bord.py) : une ligne par passage, {"t": "2026-10-13T23:00", "v": {"<id>": <vues>}}. C'est la forme mesurée que le parser lit ; le schéma de collecte.py n'est pas touché.

Décisions, mesurées

  • Un passage ne sert qu'une fois : le jalon J+1 prend le premier passage dont l'âge atteint 1 jour, J+7 cherche après lui, J+28 après encore. Les âges rendus montent toujours ; un long relevé une seule fois ne pèse pas trois fois dans l'ajustement.
  • Un jalon sans mesure vaut null, jamais 0 (un zéro relevé, lui, est une mesure et sort en 0).
  • Pas de dépendance ajoutée : le crate ne dépend que de serde_json, donc Cargo.lock ne bouge pas et --locked tient. Les deux types sont lus à la main depuis serde_json::Value, comme conso.rs.
  • ajuster() porte onglet_projection.ajuster() : pente log-log bornée [0,05 ; 1,5], quartiles des résidus, extrêmes si elargie.

Preuves

  • cargo test --release --locked : 4 + 18 + 8 + 13 tests, test result: ok, 0 échec (collecteur), plus les bancs du service inchangés.
  • cargo build --release --locked : aucun avertissement.
  • python3 _tableau/garde_fous.py : « rien à redire » (code 0).

Hors périmètre, comme demandé

Aucun fichier Python touché (onglet_projection.py, collecte.py, le front : intacts). Le câblage de la courbe dans l'onglet Projection et l'affichage Svelte restent sous t_db44afed / t_b50a3d24.

Carte t_1bfed611. Porte la courbe de vie LONGITUDINALE sur le backend Rust, en Python inchangé. ## Ce qui est livré - `rust/collecteur/src/vie_longue.rs` : `Passage`/`Entree`, `cumuls()`, `ajuster()`, `taux_heures_vues()`, `points()`, `format_de()`, et la page `page()`. - `rust/collecteur/tests/vie_longue.rs` + `tests/fixtures/vie-longue/` (3 vidéos, 3 passages) : 13 tests, dont un qui lance le binaire. - `main.rs` : sous-commande `--vie-longue <videos-vues.jsonl> <publications.json>`, en tête de `main`, sans toucher aux autres sous-commandes. ## La forme réelle du fichier (le mandat la supposait autre) Le mandat annonçait `{"date": "...", "videos": [{"id", "vues"}]}`. Mesuré sur les copies du 132 disponibles le 2026-10-14 (`/home/atelier/tmp-relecteur/*/videos-vues.jsonl`, et l'écriture de `VUES_VIDEOS` dans `tableau-bord.py`) : une ligne par passage, `{"t": "2026-10-13T23:00", "v": {"<id>": <vues>}}`. C'est la forme mesurée que le parser lit ; le schéma de `collecte.py` n'est pas touché. ## Décisions, mesurées - **Un passage ne sert qu'une fois** : le jalon J+1 prend le premier passage dont l'âge atteint 1 jour, J+7 cherche après lui, J+28 après encore. Les âges rendus montent toujours ; un long relevé une seule fois ne pèse pas trois fois dans l'ajustement. - **Un jalon sans mesure vaut `null`**, jamais 0 (un zéro relevé, lui, est une mesure et sort en 0). - **Pas de dépendance ajoutée** : le crate ne dépend que de `serde_json`, donc `Cargo.lock` ne bouge pas et `--locked` tient. Les deux types sont lus à la main depuis `serde_json::Value`, comme `conso.rs`. - `ajuster()` porte `onglet_projection.ajuster()` : pente log-log bornée [0,05 ; 1,5], quartiles des résidus, extrêmes si `elargie`. ## Preuves - `cargo test --release --locked` : 4 + 18 + 8 + 13 tests, `test result: ok`, 0 échec (collecteur), plus les bancs du service inchangés. - `cargo build --release --locked` : aucun avertissement. - `python3 _tableau/garde_fous.py` : « rien à redire » (code 0). ## Hors périmètre, comme demandé Aucun fichier Python touché (`onglet_projection.py`, `collecte.py`, le front : intacts). Le câblage de la courbe dans l'onglet Projection et l'affichage Svelte restent sous t_db44afed / t_b50a3d24.
Vie longue Rust : les vues cumulées d'un long à J+1, J+7 et J+28 (STUD-602)
All checks were successful
Tests du tableau (Rust + Svelte) / Rust (_tableau/rust) (push) Successful in 25s
Tests du tableau (Rust + Svelte) / Front Svelte (_tableau/front) (push) Successful in 14s
Tests du tableau (Rust + Svelte) / Rust (_tableau/rust) (pull_request) Successful in 21s
Tests du tableau (Rust + Svelte) / Front Svelte (_tableau/front) (pull_request) Successful in 13s
b6cb3409fb
Cause : la courbe de vie de l'onglet Projection est transversale — un point par long, tous âges
confondus — donc la forme de la croissance n'y est mesurée qu'entre vidéos, jamais le long d'une
seule. Le mandat t_1bfed611 demande la mesure longitudinale sur `videos-vues.jsonl`, côté Rust,
en Python inchangé.

Remède : le module `collecteur::vie_longue` lit la forme RÉELLE du fichier, relevée sur les copies
du 132 (`{"t": "2026-10-13T23:00", "v": {"<id>": vues}}`, une ligne par passage ; le mandat la
supposait `{"date": ..., "videos": [...]}`). Pour chaque long, il relève le premier passage dont
l'âge atteint J+1, J+7 puis J+28 — un passage ne sert qu'une fois, donc les âges montent toujours et
un jalon sans relevé neuf vaut null, jamais zéro. L'ajustement est celui de
`onglet_projection.ajuster()` : moindres carrés en log-log, pente bornée [0,05 ; 1,5], quartiles des
résidus (extrêmes si élargie). Le taux heures/vues est mesuré sur les vidéos présentes des deux
côtés. La sous-commande `--vie-longue <jsonl> <publications.json>` sort la page JSON sur stdout.

Aucune dépendance ajoutée (`Cargo.lock` intact, `--locked` tient), aucun fichier Python touché.
Banc : `rust/collecteur/tests/vie_longue.rs`, 13 tests sur les fixtures (3 vidéos, 3 passages), dont
un qui lance le binaire.
hermes-fusion deleted branch hermes/vie-longue-rust 2026-10-14 22:45:03 +02:00
Collaborator

Fusionnée automatiquement par fusion-auto (forge) : CI verte (Rust et Front Svelte), PR relue (étiquette relu), sans conflit.

Fusionnée automatiquement par `fusion-auto` (forge) : CI verte (Rust et Front Svelte), PR relue (étiquette `relu`), sans conflit.
Author
Collaborator

Relu, conforme — 8/10. Relecture du 2026-10-14 par le relecteur (carte t_2727900f), sur la tête b6cb3409, dans un arbre dédié (wt/t_2727900f, HEAD détachée sur la tête de la branche). Étiquette relu posée.

Périmètre. git diff --stat origin/main...b6cb3409 : 6 fichiers, +662/-1, tous sous _tableau/rust/collecteur/. Aucun .py, aucun schéma : collecte.py, tableau-bord.py (l'écriture de VUES_VIDEOS, ligne 660), onglet_projection.py et Cargo.lock sont inchangés. Branche à jour de origin/main (3b006a30), 1 commit devant, 0 derrière.

Les quatre points du mandat, mesurés.

  1. Parsing des cumuls J+1/J+7/J+28 — juste. Le binaire sur les fixtures sort ages [1,8,29] (v1), [4,25,null] (v2, aucun passage à J+1 : l'âge réel 4 est porté), [10,null,null] (v3, un seul passage, servi une fois) ; vues [100,400,900], [220,800,null], [260,null,null]. Un zéro relevé sort en 0, une absence en null : les deux cas sont rejoués par le banc.
  2. Conversion vues → heures par le taux mesuré — présente : taux = 0.010459183673469388 sur les fixtures, soit exactement 20,5 h / 1 960 vues ; courbe porte A_heures/bas_heures/haut_heures = la courbe des vues × le taux (vérifié au bit près).
  3. Loi de puissance bornée [0,05 ; 1,5] et quartiles — identique à onglet_projection.ajuster(), mesuré sur les 6 points de la fixture : Python rend A=91.54511782943057, p=0.6424350214280312, bas=1.011797274121199, haut=1.123766338795036, n=6, Rust rend A=91.54511782943057, p=0.6424350214280313, bas=1.011797274121199, haut=1.1237663387950354, n=6 (même quantile par interpolation, mêmes résidus sur la pente déjà bornée, mêmes extrêmes si elargie).
  4. Tests verts, schémas intacts — cargo test --release --locked : 4 + 18 + 8 + 13 (collecteur) et 4 + 4 + 2 + 5 + 3 + 1 + 3 (service), tous ok, 0 échec. cargo build --release --locked : 0 avertissement. python3 _tableau/garde_fous.py : « rien à redire », code 0. CI Forgejo 4/4 success sur b6cb3409 (lu par l'API, pas d'après le compte rendu). Aucun emoji, aucun secret, aucun base64 dans le diff.

Front. Aucun fichier de _tableau/front/ touché : rien à regarder au navigateur, aucun onglet écrasé, aucune fonction visible retirée. vitest run : 25 fichiers, 189 tests passés ; vite build : 122 modules, ✓ built (lancés dans l'arbre de relecture, node_modules du dépôt lié).

Écarts mesurés.

  1. jour_de ignore le fuseau horaire (le seul écart qui touche la mesure de la carte). Il lit les 10 premiers caractères de l'ISO, là où onglet_projection.jour_de convertit en heure de Paris. Le commentaire du module annonce pourtant « comme onglet_projection.points() (jour_de) ». Mesure sur les publications réelles (/studio/_tableau/collecte-132.json, lu le 13/10 19 h 35) : 11 des 433 videos_pub (2,5 %) ont un pub entre 22 h et 24 h UTC, donc un jour de Paris de plus que le jour UTC. Cas rejoué de bout en bout : pub = 2026-09-30T22:30:00Z, passages 2026-10-01T23:00, 2026-10-08T23:00, 2026-10-29T23:00 → Rust rend les âges 1, 8, 29 ; la référence Python rend 0, 7, 28. Conséquence : pour ces longs, le J+1 est relevé le jour même de la publication et les trois jalons glissent d'un jour. La forme réelle de t (2026-10-13T23:00, heure locale, sans fuseau) n'est pas concernée : seul pub, fourni par l'appelant, porte un Z. Carte de suite créée pour la correction.
  2. page() sort une courbe dès 3 points (n=3 mesuré) alors que la règle du studio est MIN_MESURES = 5 : la constante est déclarée et documentée, mais appliquée nulle part — c'est l'appelant qui doit comparer (c'est écrit dans le module). À tenir au câblage.
  3. points(cumuls, None) ne retire pas les court : la courbe « studio » inclurait les Shorts si l'appelant les fournit, alors que la courbe studio de Python se construit sur tb_chaines.est_long, qui a en plus une exception par chaîne (SANS_SHORTS) que Entree ne peut pas porter (pas de nom de chaîne). À trancher au câblage, pas un défaut du mandat.
  4. Le taux heures_video/vues divise des heures de 12 mois glissants (collecte.py, d365) par des vues cumulées depuis toujours : deux fenêtres différentes, donc un taux biaisé pour les longs de plus de douze mois. C'est le mandat qui le demande ainsi (« taux heures_video/vues mesuré ») — noté, pas compté contre.

Non mesuré. Pas d'exécution sur le videos-vues.jsonl de production : le 132 est injoignable depuis le PC (no route to host), la mesure porte sur les copies présentes sur le PC et sur la lecture de tableau-bord.py ; pas de fichier de 40 jours complet rejoué. Aucun câblage dans le service ni dans l'onglet Projection (hors périmètre de la carte).

**Relu, conforme — 8/10.** Relecture du 2026-10-14 par le relecteur (carte `t_2727900f`), sur la tête `b6cb3409`, dans un arbre dédié (`wt/t_2727900f`, HEAD détachée sur la tête de la branche). Étiquette `relu` posée. **Périmètre.** `git diff --stat origin/main...b6cb3409` : 6 fichiers, +662/-1, tous sous `_tableau/rust/collecteur/`. Aucun `.py`, aucun schéma : `collecte.py`, `tableau-bord.py` (l'écriture de `VUES_VIDEOS`, ligne 660), `onglet_projection.py` et `Cargo.lock` sont inchangés. Branche à jour de `origin/main` (`3b006a30`), 1 commit devant, 0 derrière. **Les quatre points du mandat, mesurés.** 1. Parsing des cumuls J+1/J+7/J+28 — juste. Le binaire sur les fixtures sort `ages` `[1,8,29]` (v1), `[4,25,null]` (v2, aucun passage à J+1 : l'âge réel 4 est porté), `[10,null,null]` (v3, un seul passage, servi une fois) ; `vues` `[100,400,900]`, `[220,800,null]`, `[260,null,null]`. Un zéro relevé sort en `0`, une absence en `null` : les deux cas sont rejoués par le banc. 2. Conversion vues → heures par le taux mesuré — présente : `taux` = `0.010459183673469388` sur les fixtures, soit exactement 20,5 h / 1 960 vues ; `courbe` porte `A_heures`/`bas_heures`/`haut_heures` = la courbe des vues × le taux (vérifié au bit près). 3. Loi de puissance bornée [0,05 ; 1,5] et quartiles — **identique à `onglet_projection.ajuster()`**, mesuré sur les 6 points de la fixture : Python rend `A=91.54511782943057, p=0.6424350214280312, bas=1.011797274121199, haut=1.123766338795036, n=6`, Rust rend `A=91.54511782943057, p=0.6424350214280313, bas=1.011797274121199, haut=1.1237663387950354, n=6` (même `quantile` par interpolation, mêmes résidus sur la pente déjà bornée, mêmes extrêmes si `elargie`). 4. Tests verts, schémas intacts — `cargo test --release --locked` : 4 + 18 + 8 + 13 (collecteur) et 4 + 4 + 2 + 5 + 3 + 1 + 3 (service), tous `ok`, **0 échec**. `cargo build --release --locked` : **0 avertissement**. `python3 _tableau/garde_fous.py` : « rien à redire », code 0. CI Forgejo **4/4 `success` sur `b6cb3409`** (lu par l'API, pas d'après le compte rendu). Aucun emoji, aucun secret, aucun base64 dans le diff. **Front.** Aucun fichier de `_tableau/front/` touché : rien à regarder au navigateur, aucun onglet écrasé, aucune fonction visible retirée. `vitest run` : 25 fichiers, **189 tests passés** ; `vite build` : 122 modules, `✓ built` (lancés dans l'arbre de relecture, `node_modules` du dépôt lié). **Écarts mesurés.** 1. **`jour_de` ignore le fuseau horaire** (le seul écart qui touche la mesure de la carte). Il lit les 10 premiers caractères de l'ISO, là où `onglet_projection.jour_de` convertit en heure de Paris. Le commentaire du module annonce pourtant « comme `onglet_projection.points()` (`jour_de`) ». Mesure sur les publications réelles (`/studio/_tableau/collecte-132.json`, lu le 13/10 19 h 35) : **11 des 433 `videos_pub` (2,5 %) ont un `pub` entre 22 h et 24 h UTC**, donc un jour de Paris de plus que le jour UTC. Cas rejoué de bout en bout : `pub = 2026-09-30T22:30:00Z`, passages `2026-10-01T23:00`, `2026-10-08T23:00`, `2026-10-29T23:00` → Rust rend les âges `1, 8, 29` ; la référence Python rend `0, 7, 28`. Conséquence : pour ces longs, le J+1 est relevé le jour même de la publication et les trois jalons glissent d'un jour. La forme réelle de `t` (`2026-10-13T23:00`, heure locale, sans fuseau) n'est pas concernée : seul `pub`, fourni par l'appelant, porte un `Z`. Carte de suite créée pour la correction. 2. `page()` sort une courbe dès 3 points (`n=3` mesuré) alors que la règle du studio est `MIN_MESURES = 5` : la constante est déclarée et documentée, mais appliquée nulle part — c'est l'appelant qui doit comparer (c'est écrit dans le module). À tenir au câblage. 3. `points(cumuls, None)` ne retire pas les `court` : la courbe « studio » inclurait les Shorts si l'appelant les fournit, alors que la courbe studio de Python se construit sur `tb_chaines.est_long`, qui a en plus une exception par chaîne (`SANS_SHORTS`) que `Entree` ne peut pas porter (pas de nom de chaîne). À trancher au câblage, pas un défaut du mandat. 4. Le taux `heures_video`/`vues` divise des heures de **12 mois glissants** (`collecte.py`, `d365`) par des vues **cumulées depuis toujours** : deux fenêtres différentes, donc un taux biaisé pour les longs de plus de douze mois. C'est le mandat qui le demande ainsi (« taux `heures_video`/`vues` mesuré ») — noté, pas compté contre. **Non mesuré.** Pas d'exécution sur le `videos-vues.jsonl` de production : le 132 est injoignable depuis le PC (`no route to host`), la mesure porte sur les copies présentes sur le PC et sur la lecture de `tableau-bord.py` ; pas de fichier de 40 jours complet rejoué. Aucun câblage dans le service ni dans l'onglet Projection (hors périmètre de la carte).
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
foXaCe/studio!1664
No description provided.