feat(agent): aggregate Claude usage from local transcripts (#49) #51

Merged
maximus merged 3 commits from issue-49-claude-usage-aggregate into main 2026-10-04 19:29:38 +00:00
Owner

Summary

Resolves #49. Adds agent/claude-usage/aggregate.js, the pure aggregation module of the claude-usage agent: it reads the Claude Code transcripts (~/.claude/projects/**/*.jsonl, subagents included) and produces the 30-day per-model series and the top projects of the schemaVersion: 1 contract. No network, no runtime dependency, nothing written.

aggregate({ root, frozenDays = {}, today = <Toronto today>, homedir = os.homedir(), openFile = fs.createReadStream })
  -> Promise<{ openDays, days, projects, claudeCodeVersion }>
  • openDays: { "YYYY-MM-DD": { models: {<model>: {input, output, cacheCreation, cacheRead, requests}}, projects: {<name>: {work, cacheRead, requests}} } } for every non-frozen window day, empty days included (so the agent can freeze them all).
  • days: exactly 30 { date, models }, oldest first, newest = today; frozen days served from frozenDays; at most 12 models per day (rest folded into other).
  • projects: top 8 by work tokens (input + output + cacheCreation) over the window, then (autres) when anything remains; the sum of work equals the window total.
  • claudeCodeVersion: version of the newest line read, or null.

Also exported: mapProject, torontoDay, shiftDay, windowDays, capModels, topProjects, listTranscripts and the constants.

Decisions

  • [HIGH] Scan threshold = day after the most recent frozen day, floored at the window start (a file last written before the window cannot hold a line in it).
  • [HIGH] Model cap applied to days only; openDays keeps the raw per-model counts for freezing. capModels() is idempotent.
  • [MEDIUM] A model name outside the server key pattern (^[a-z0-9][a-z0-9.\-]{0,63}$) folds into other rather than being dropped by the server, so volume and the projects/series totals stay consistent.
  • [MEDIUM] A line missing message.id or requestId counts as its own request (measured presence is 100 %, fallback only).
  • [HIGH] A transcript that fails to read mid-run is skipped. Found while testing: readline forwards the input error to the interface, where an unlistened error is an uncaught exception.
  • [HIGH] Frozen days are re-read through the same counters (non-safe-integer -> 0), never trusted verbatim.

Tests

  • __tests__/agent-claude-usage.test.js: 38 cases (dedup across session/subagents, synthetic/user/truncated lines, 23:59 vs 00:01 Toronto incl. both midnights around the 2026-11-01 DST change, one case per mapping rule, 30-entry series, top 8 + (autres) sums, 12-model cap, spy on file opening below the threshold, both injected and vi.spyOn(fs, "createReadStream")).
  • Synthetic fixtures only under __tests__/fixtures/claude-usage/; mtimes set with utimesSync on a temp copy.
  • npm test: 415 passed (9 files). bash test-curl.sh: 45 passed. sh scripts/boot-from-copy.sh: PASS.
  • No doc, Dockerfile or existing test touched (docs land with #48 and #50).

Generated autonomously by /autopilot run of 2026-10-04

🤖 Generated with Claude Code

## Summary Resolves #49. Adds `agent/claude-usage/aggregate.js`, the pure aggregation module of the `claude-usage` agent: it reads the Claude Code transcripts (`~/.claude/projects/**/*.jsonl`, subagents included) and produces the 30-day per-model series and the top projects of the `schemaVersion: 1` contract. No network, no runtime dependency, nothing written. ```js aggregate({ root, frozenDays = {}, today = <Toronto today>, homedir = os.homedir(), openFile = fs.createReadStream }) -> Promise<{ openDays, days, projects, claudeCodeVersion }> ``` - `openDays`: `{ "YYYY-MM-DD": { models: {<model>: {input, output, cacheCreation, cacheRead, requests}}, projects: {<name>: {work, cacheRead, requests}} } }` for every non-frozen window day, empty days included (so the agent can freeze them all). - `days`: exactly 30 `{ date, models }`, oldest first, newest = today; frozen days served from `frozenDays`; at most 12 models per day (rest folded into `other`). - `projects`: top 8 by work tokens (input + output + cacheCreation) over the window, then `(autres)` when anything remains; the sum of `work` equals the window total. - `claudeCodeVersion`: `version` of the newest line read, or `null`. Also exported: `mapProject`, `torontoDay`, `shiftDay`, `windowDays`, `capModels`, `topProjects`, `listTranscripts` and the constants. ## Decisions - [HIGH] Scan threshold = day after the most recent frozen day, floored at the window start (a file last written before the window cannot hold a line in it). - [HIGH] Model cap applied to `days` only; `openDays` keeps the raw per-model counts for freezing. `capModels()` is idempotent. - [MEDIUM] A model name outside the server key pattern (`^[a-z0-9][a-z0-9.\-]{0,63}$`) folds into `other` rather than being dropped by the server, so volume and the projects/series totals stay consistent. - [MEDIUM] A line missing `message.id` or `requestId` counts as its own request (measured presence is 100 %, fallback only). - [HIGH] A transcript that fails to read mid-run is skipped. Found while testing: readline forwards the input error to the interface, where an unlistened `error` is an uncaught exception. - [HIGH] Frozen days are re-read through the same counters (non-safe-integer -> 0), never trusted verbatim. ## Tests - `__tests__/agent-claude-usage.test.js`: 38 cases (dedup across session/subagents, synthetic/user/truncated lines, 23:59 vs 00:01 Toronto incl. both midnights around the 2026-11-01 DST change, one case per mapping rule, 30-entry series, top 8 + `(autres)` sums, 12-model cap, spy on file opening below the threshold, both injected and `vi.spyOn(fs, "createReadStream")`). - Synthetic fixtures only under `__tests__/fixtures/claude-usage/`; mtimes set with `utimesSync` on a temp copy. - `npm test`: 415 passed (9 files). `bash test-curl.sh`: 45 passed. `sh scripts/boot-from-copy.sh`: PASS. - No doc, Dockerfile or existing test touched (docs land with #48 and #50). Generated autonomously by /autopilot run of 2026-10-04 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-10-04 13:11:12 +00:00
feat(agent): aggregate Claude usage from local transcripts
All checks were successful
CI / verification (pull_request) Successful in 28s
c8f92ab25f
Pure, dependency-free module for the claude-usage agent. It walks
~/.claude/projects/**/*.jsonl (subagent transcripts included), keeps
assistant lines with message.usage, drops <synthetic> and truncated
lines, deduplicates on (message.id, requestId) across all files at the
largest output_tokens, and buckets by America/Toronto day computed as
strings only (DST-safe).

Returns { openDays, days, projects, claudeCodeVersion }: the recomputed
non-frozen days, the merged 30-day series (at most 12 models per day,
the rest folded into "other"), the top 8 projects by work tokens plus
"(autres)", and the version of the newest line. Files whose mtime day
is before the day after the most recent frozen day are never opened.

Resolves #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus added the
autopilot:pending-human
label 2026-10-04 13:11:20 +00:00
maximus added 1 commit 2026-10-04 13:11:48 +00:00
fix(agent): collapse doubled slashes in cwd before project mapping
All checks were successful
CI / verification (pull_request) Successful in 26s
d7f209f0ca
Resolves #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Self-review (autopilot)

Diff git diff main...HEAD checked against every acceptance criterion of #49:

  • Duplicates split between a session file and subagents/ count once, at the largest output_tokens (fixture: 8 / 1024 in the session, 2048 / 512 in the subagent -> 2048, 1 request).
  • <synthetic>, user lines (even with a usage object) and truncated JSON ignored without exception.
  • 23:59 / 00:01 Toronto on two days, at EDT and at both midnights around the 2026-11-01 change (incl. 04:30Z on Nov 2 = Nov 1 EST).
  • One test per project mapping rule (1 incl. .spikes/.archive, 2, 3, 4, 5), homedir injected.
  • Series always 30 entries, empty days included, newest = Toronto today (also across DST).
  • Top 8 + (autres): sum of work equals the window total, cross-checked against the series.
  • With a frozen day, no file whose mtime day is before the threshold is opened (injected opener + vi.spyOn(fs, 'createReadStream')), boundary checked at Oct 1 23:30 vs Oct 2 00:30 Toronto.

Found and fixed during review: a cwd with a doubled slash yielded an empty project name (d7f209f). Found while testing: readline re-emits a stream error on the interface (uncaught exception); both are now listened, an unreadable transcript is skipped.

Out of scope, noted: agent/claude-usage/ is not in the Dockerfile COPY (workstation code, as agent/); docs are left to #48/#50.

Verdict: ready for human review. npm test 415/415, test-curl.sh 45/45, boot-from-copy PASS.

## Self-review (autopilot) Diff `git diff main...HEAD` checked against every acceptance criterion of #49: - [x] Duplicates split between a session file and `subagents/` count once, at the largest `output_tokens` (fixture: 8 / 1024 in the session, 2048 / 512 in the subagent -> 2048, 1 request). - [x] `<synthetic>`, `user` lines (even with a `usage` object) and truncated JSON ignored without exception. - [x] 23:59 / 00:01 Toronto on two days, at EDT and at both midnights around the 2026-11-01 change (incl. 04:30Z on Nov 2 = Nov 1 EST). - [x] One test per project mapping rule (1 incl. `.spikes`/`.archive`, 2, 3, 4, 5), homedir injected. - [x] Series always 30 entries, empty days included, newest = Toronto today (also across DST). - [x] Top 8 + `(autres)`: sum of `work` equals the window total, cross-checked against the series. - [x] With a frozen day, no file whose mtime day is before the threshold is opened (injected opener + `vi.spyOn(fs, 'createReadStream')`), boundary checked at Oct 1 23:30 vs Oct 2 00:30 Toronto. Found and fixed during review: a `cwd` with a doubled slash yielded an empty project name (d7f209f). Found while testing: readline re-emits a stream error on the interface (uncaught exception); both are now listened, an unreadable transcript is skipped. Out of scope, noted: `agent/claude-usage/` is not in the Dockerfile `COPY` (workstation code, as `agent/`); docs are left to #48/#50. Verdict: ready for human review. npm test 415/415, test-curl.sh 45/45, boot-from-copy PASS.
maximus changed target branch from main to issue-48-claude-usage-routes 2026-10-04 13:18:19 +00:00
maximus force-pushed issue-49-claude-usage-aggregate from d7f209f0ca to 5cb9182911 2026-10-04 13:18:21 +00:00 Compare
Author
Owner

Orchestrateur autopilot : branche rebasée sur issue-48-claude-usage-routes (PR #52) et base de la PR retargetée, pour former la pile #52 → #51 → (#50). Rebase sans conflit (fichiers disjoints). Tip cumulé validé : vitest 538/538, test-curl 45/45, boot-from-copy PASS. Nouveau head : 5cb9182.

Orchestrateur autopilot : branche rebasée sur `issue-48-claude-usage-routes` (PR #52) et base de la PR retargetée, pour former la pile #52 → #51 → (#50). Rebase sans conflit (fichiers disjoints). Tip cumulé validé : vitest 538/538, test-curl 45/45, boot-from-copy PASS. Nouveau head : 5cb9182.
Author
Owner

Verdict : REQUEST_CHANGES

Module propre et bien teste dans l'ensemble (dates en chaines, dedup inter-fichiers, readline protege contre l'erreur d'entree, compteurs re-valides). Deux defauts de correction, tous deux reproduits sur une copie hors depot de la tete 5cb9182.

Issues

  1. agent/claude-usage/aggregate.js:311-316 : le seuil de scan suppose des jours geles contigus et anterieurs a today. Le seuil vaut « jour apres le plus recent jour gele », calcule sur toutes les cles de frozenDays, meme hors fenetre ou dans le futur. Tout jour ouvert anterieur a ce seuil est alors rendu vide dans openDays sans qu'aucun fichier ne soit lu, et l'appelant (#50) le gelera vide pour de bon. Reproduit :

    • transcript avec une ligne le 2026-10-03, mtime le 2026-10-03, today = 2026-10-04 : sans gel, openDays["2026-10-03"] compte la requete ;
    • meme arbre avec frozenDays = {"2026-11-15": …} (horloge du ThinkPad en avance un jour, etat d'une autre machine) : openDays["2026-10-03"].models vaut {} ;
    • avec frozenDays = {"2026-10-04": …} (un trou : today gele, la veille ouverte) : {} aussi.

    Le seuil correct par construction est le plus ancien jour ouvert (openDayList[0], rien a lire si la liste est vide) : un fichier dont le mtime precede le premier jour ouvert ne peut contenir aucune ligne d'un jour ouvert, quel que soit l'agencement des jours geles. Sur un etat contigu, il coincide avec la regle actuelle (meme gain de perf). Mutation faite : cette regle fait tomber 2 cas sur 38, les deux du bloc frozen days, parce que leur fixture FROZEN ne gele que 09-30 et 10-01 et laisse 09-05..09-29 ouverts : le test epingle le trou (old.jsonl, mtime 09-20, doit rester ferme alors que le 09-20 est un jour ouvert rendu vide). A corriger avec le code : geler toute la fenetre jusqu'au 10-01 dans la fixture, et ajouter un cas « jour gele futur » et un cas « trou ».

  2. aggregate.js:387-410 et normalizeDay() (:194-210) : cles de projet sur objets litteraux {}. Un cwd <home>/claude-code/__proto__ ou <home>/claude-code/constructor (ou une cle __proto__ dans l'etat gele relu par JSON.parse) fait que bucket.projects[name] renvoie Object.prototype / Object, qui est truthy : addProject ecrit alors work, cacheRead, requests = NaN sur Object.prototype et la consommation du projet disparait du top (Object.entries ne la voit pas). Reproduit : apres un passage, ({}).work === NaN et Object.work === NaN, les deux projets absents de projects. Les noms de modeles sont epargnes par MODEL_KEY_RE (pas de _), mais toString/valueOf comme nom de dossier le sont aussi pour les projets. Le CLAUDE.md du depot pose deja cette regle cote serveur (« une cle __proto__ … ne doit jamais arriver la ») ; l'agent qui produit le payload doit la tenir aussi. Correctif : Object.create(null) pour openDays[*].projects, projectTotals et les deux objets de normalizeDay() (ou des Map), plus un cas de test avec __proto__ et constructor en cwd et en cle d'etat gele, qui verifie aussi ({}).work === undefined apres le passage.

Suggestions (non bloquantes)

  • aggregate.js:349-351 : le filtre « jour gele » passe avant la dedup. Une requete dont les lignes chevauchent minuit (premiere ligne 23:59:59 sur un jour gele, ligne finale 00:00:01) est comptee une seconde fois sur le jour ouvert. Marginal ; dedupliquer d'abord, puis filtrer sur le jour de la ligne retenue, le ferme.
  • Le corps annonce « 415 passed (9 files) » ; la tete cumulee donne 538 (10 fichiers), la base #48 apportant ses propres cas. Rien a corriger, a mettre a jour dans le corps.

Verifications faites

  • npx vitest run sur la tete extraite (git archive) dans le scratchpad : 538/538 verts.
  • Mutations du relecteur sur la copie : regle « premier jour ouvert » -> 2 rouges (epingle du trou, cf. issue 1) ; dedup sans « plus grand output » -> 3 rouges ; seuil mtime retire -> 3 rouges. Copie restauree puis supprimee (rm -rf), aucun fichier du depot touche.
  • Commits conventionnels, Resolves #49 dans les deux messages ; la timeline de #49 porte un closes venant de cette PR.
## Verdict : REQUEST_CHANGES Module propre et bien teste dans l'ensemble (dates en chaines, dedup inter-fichiers, `readline` protege contre l'erreur d'entree, compteurs re-valides). Deux defauts de correction, tous deux reproduits sur une copie hors depot de la tete `5cb9182`. ### Issues 1. **`agent/claude-usage/aggregate.js:311-316` : le seuil de scan suppose des jours geles contigus et anterieurs a `today`.** Le seuil vaut « jour apres le plus recent jour gele », calcule sur **toutes** les cles de `frozenDays`, meme hors fenetre ou dans le futur. Tout jour ouvert anterieur a ce seuil est alors rendu **vide** dans `openDays` sans qu'aucun fichier ne soit lu, et l'appelant (#50) le gelera vide pour de bon. Reproduit : - transcript avec une ligne le 2026-10-03, mtime le 2026-10-03, `today = 2026-10-04` : sans gel, `openDays["2026-10-03"]` compte la requete ; - meme arbre avec `frozenDays = {"2026-11-15": …}` (horloge du ThinkPad en avance un jour, etat d'une autre machine) : `openDays["2026-10-03"].models` vaut `{}` ; - avec `frozenDays = {"2026-10-04": …}` (un trou : today gele, la veille ouverte) : `{}` aussi. Le seuil correct par construction est **le plus ancien jour ouvert** (`openDayList[0]`, rien a lire si la liste est vide) : un fichier dont le mtime precede le premier jour ouvert ne peut contenir aucune ligne d'un jour ouvert, quel que soit l'agencement des jours geles. Sur un etat contigu, il coincide avec la regle actuelle (meme gain de perf). Mutation faite : cette regle fait tomber 2 cas sur 38, les deux du bloc `frozen days`, parce que leur fixture `FROZEN` ne gele que 09-30 et 10-01 et laisse 09-05..09-29 ouverts : **le test epingle le trou** (`old.jsonl`, mtime 09-20, doit rester ferme alors que le 09-20 est un jour ouvert rendu vide). A corriger avec le code : geler toute la fenetre jusqu'au 10-01 dans la fixture, et ajouter un cas « jour gele futur » et un cas « trou ». 2. **`aggregate.js:387-410` et `normalizeDay()` (`:194-210`) : cles de projet sur objets litteraux `{}`.** Un `cwd` `<home>/claude-code/__proto__` ou `<home>/claude-code/constructor` (ou une cle `__proto__` dans l'etat gele relu par `JSON.parse`) fait que `bucket.projects[name]` renvoie `Object.prototype` / `Object`, qui est truthy : `addProject` ecrit alors `work`, `cacheRead`, `requests` = `NaN` **sur `Object.prototype`** et la consommation du projet disparait du top (`Object.entries` ne la voit pas). Reproduit : apres un passage, `({}).work === NaN` et `Object.work === NaN`, les deux projets absents de `projects`. Les noms de modeles sont epargnes par `MODEL_KEY_RE` (pas de `_`), mais `toString`/`valueOf` comme nom de dossier le sont aussi pour les projets. Le CLAUDE.md du depot pose deja cette regle cote serveur (« une cle `__proto__` … ne doit jamais arriver la ») ; l'agent qui produit le payload doit la tenir aussi. Correctif : `Object.create(null)` pour `openDays[*].projects`, `projectTotals` et les deux objets de `normalizeDay()` (ou des `Map`), plus un cas de test avec `__proto__` et `constructor` en `cwd` et en cle d'etat gele, qui verifie aussi `({}).work === undefined` apres le passage. ### Suggestions (non bloquantes) - `aggregate.js:349-351` : le filtre « jour gele » passe avant la dedup. Une requete dont les lignes chevauchent minuit (premiere ligne 23:59:59 sur un jour gele, ligne finale 00:00:01) est comptee une seconde fois sur le jour ouvert. Marginal ; dedupliquer d'abord, puis filtrer sur le jour de la ligne retenue, le ferme. - Le corps annonce « 415 passed (9 files) » ; la tete cumulee donne 538 (10 fichiers), la base #48 apportant ses propres cas. Rien a corriger, a mettre a jour dans le corps. ### Verifications faites - `npx vitest run` sur la tete extraite (`git archive`) dans le scratchpad : 538/538 verts. - Mutations du relecteur sur la copie : regle « premier jour ouvert » -> 2 rouges (epingle du trou, cf. issue 1) ; dedup sans « plus grand `output` » -> 3 rouges ; seuil mtime retire -> 3 rouges. Copie restauree puis supprimee (`rm -rf`), aucun fichier du depot touche. - Commits conventionnels, `Resolves #49` dans les deux messages ; la timeline de #49 porte un `closes` venant de cette PR.
maximus added 1 commit 2026-10-04 19:25:44 +00:00
fix(agent): take the scan threshold from the open days, key maps without prototype
Some checks are pending
CI / verification (pull_request) Successful in 28s
CI / verification (push) Waiting to run
c888f7c633
The threshold came from the most recent frozen key: a frozen day in the
future, or a gap in the frozen run, pushed it past open days whose files
were then never read, and the agent would have frozen them empty for good.
It is now the oldest open day of the window (the same day as before for the
agent's contiguous frozen prefix).

Project, model and day maps are now Object.create(null): a project named
__proto__ or constructor wrote NaN counters onto Object.prototype and
dropped out of the top.

Resolves #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Correctif des deux points bloquants de la revue (commit c888f7c) :

  1. Seuil de scan : pris sur le plus ancien jour ouvert de la fenetre (openDayList[0], ou lendemain d aujourd hui si tout est fige), plus sur la derniere cle figee. Fixture FROZEN corrigee pour ce que l agent fournit reellement (serie figee contigue depuis le debut de la fenetre). Nouveaux cas : jour fige futur, trou dans la serie figee, tout fige.
  2. Prototype : toutes les maps indexees par un nom (modeles, projets, jours, totaux) passent par dict() = Object.create(null). Nouveaux cas : projets __proto__ / constructor en transcript et en jour fige, avec controle que Object.prototype et Object restent sans work/cacheRead/...

Les 4 nouveaux cas echouent sur l ancien code (verifie par stash) et passent sur le nouveau. Tip de pile : vitest 543/543 sur cette branche, test-curl 45/45, boot-from-copy PASS.

Non traite (non bloquant) : double compte possible d une requete a cheval sur minuit quand le premier jour est fige.

La PR #53 a ete rebasee sur ce commit.

Correctif des deux points bloquants de la revue (commit `c888f7c`) : 1. **Seuil de scan** : pris sur le plus ancien jour ouvert de la fenetre (`openDayList[0]`, ou lendemain d aujourd hui si tout est fige), plus sur la derniere cle figee. Fixture `FROZEN` corrigee pour ce que l agent fournit reellement (serie figee contigue depuis le debut de la fenetre). Nouveaux cas : jour fige futur, trou dans la serie figee, tout fige. 2. **Prototype** : toutes les maps indexees par un nom (modeles, projets, jours, totaux) passent par `dict()` = `Object.create(null)`. Nouveaux cas : projets `__proto__` / `constructor` en transcript et en jour fige, avec controle que `Object.prototype` et `Object` restent sans `work`/`cacheRead`/... Les 4 nouveaux cas echouent sur l ancien code (verifie par stash) et passent sur le nouveau. Tip de pile : vitest 543/543 sur cette branche, test-curl 45/45, boot-from-copy PASS. Non traite (non bloquant) : double compte possible d une requete a cheval sur minuit quand le premier jour est fige. La PR #53 a ete rebasee sur ce commit.
Author
Owner

Verdict : APPROVE (re-revue de c888f7c)

Les deux points bloquants de la passe precedente sont corriges par construction, et chaque correctif est epingle : reintroduire l'ancien defaut fait rougir les nouveaux cas.

Points bloquants precedents

  1. Seuil de scan (aggregate.js:325) : threshold = openDayList[0], ou le lendemain d'aujourd'hui si tout est gele. Un jour gele futur ou un trou ne peut plus vider un jour ouvert. La fixture FROZEN gele desormais une serie contigue depuis le debut de la fenetre, ce qui retire l'epingle du trou signalee. Trois cas ajoutes : jour gele futur, trou, tout gele.
  2. Prototype : dict() = Object.create(null) sur toutes les maps indexees par un nom (jours geles, openDays, modeles et projets, projectTotals, normalizeDay, capModels). Des cas __proto__ / constructor couvrent le transcript et l'etat gele, avec controle de Object.prototype et Object.

Suggestions (non bloquantes)

  • openDays[*].models / .projects sont maintenant sans prototype : l'appelant (#50) ne doit pas y appeler obj.hasOwnProperty(...) (TypeError), seulement Object.prototype.hasOwnProperty.call ou Object.hasOwn. JSON.stringify les serialise normalement. A garder en tete en revue de #50.
  • Toujours ouvert, comme annonce : une requete a cheval sur minuit dont la premiere ligne tombe sur un jour gele peut etre comptee une seconde fois sur le jour ouvert (filtre avant dedup). Marginal.
  • Le corps annonce encore « 38 cases / 415 passed » : la tete compte 43 cas dans ce fichier, 543 au total.

Verifications faites

  • Tete c888f7c extraite par git archive dans le scratchpad : npx vitest run 543/543 verts.

  • Mutations du relecteur sur la copie (fichier de test cible, 43 cas) :

    • ancien seuil « dernier jour gele + 1 » : 2 rouges ;
    • dict() rendu en {} : 2 rouges ;
    • {} dans normalizeDay seul : 1 rouge ;
    • projectTotals = {} : 2 rouges ;
    • openDays[*].projects = {} : 2 rouges ;
    • seuil « tout gele » ramene a windowStart : 1 rouge ;
    • filtre « jour gele » retire de onLine : 2 rouges.

    Le fichier a ete restaure apres chaque mutation (cmp identique), puis la copie supprimee.

  • Commits conventionnels, Resolves #49 ; la timeline de #49 porte un closes venant de #51 (et none de #53).

  • CI de la tete encore pending au moment de la revue ; le verdict repose sur la suite et les mutations rejouees localement.

## Verdict : APPROVE (re-revue de `c888f7c`) Les deux points bloquants de la passe precedente sont corriges par construction, et chaque correctif est epingle : reintroduire l'ancien defaut fait rougir les nouveaux cas. ### Points bloquants precedents 1. **Seuil de scan** (`aggregate.js:325`) : `threshold = openDayList[0]`, ou le lendemain d'aujourd'hui si tout est gele. Un jour gele futur ou un trou ne peut plus vider un jour ouvert. La fixture `FROZEN` gele desormais une serie contigue depuis le debut de la fenetre, ce qui retire l'epingle du trou signalee. Trois cas ajoutes : jour gele futur, trou, tout gele. 2. **Prototype** : `dict()` = `Object.create(null)` sur toutes les maps indexees par un nom (jours geles, `openDays`, modeles et projets, `projectTotals`, `normalizeDay`, `capModels`). Des cas `__proto__` / `constructor` couvrent le transcript et l'etat gele, avec controle de `Object.prototype` et `Object`. ### Suggestions (non bloquantes) - `openDays[*].models` / `.projects` sont maintenant sans prototype : l'appelant (#50) ne doit pas y appeler `obj.hasOwnProperty(...)` (TypeError), seulement `Object.prototype.hasOwnProperty.call` ou `Object.hasOwn`. `JSON.stringify` les serialise normalement. A garder en tete en revue de #50. - Toujours ouvert, comme annonce : une requete a cheval sur minuit dont la premiere ligne tombe sur un jour gele peut etre comptee une seconde fois sur le jour ouvert (filtre avant dedup). Marginal. - Le corps annonce encore « 38 cases / 415 passed » : la tete compte 43 cas dans ce fichier, 543 au total. ### Verifications faites - Tete `c888f7c` extraite par `git archive` dans le scratchpad : `npx vitest run` 543/543 verts. - Mutations du relecteur sur la copie (fichier de test cible, 43 cas) : - ancien seuil « dernier jour gele + 1 » : 2 rouges ; - `dict()` rendu en `{}` : 2 rouges ; - `{}` dans `normalizeDay` seul : 1 rouge ; - `projectTotals = {}` : 2 rouges ; - `openDays[*].projects = {}` : 2 rouges ; - seuil « tout gele » ramene a `windowStart` : 1 rouge ; - filtre « jour gele » retire de `onLine` : 2 rouges. Le fichier a ete restaure apres chaque mutation (`cmp` identique), puis la copie supprimee. - Commits conventionnels, `Resolves #49` ; la timeline de #49 porte un `closes` venant de #51 (et `none` de #53). - CI de la tete encore `pending` au moment de la revue ; le verdict repose sur la suite et les mutations rejouees localement.
maximus changed target branch from issue-48-claude-usage-routes to main 2026-10-04 19:29:29 +00:00
maximus merged commit c888f7c633 into main 2026-10-04 19:29:38 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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: maximus/vps-health-api#51
No description provided.