diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eca3179..89c9bdd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,6 +33,12 @@ updates: - dependency-name: '@huggingface/transformers' versions: - '>=4.3.0 <4.4.0' + # V52.3: TypeScript 7 typechecks this code with 0 errors, but + # typescript-eslint 8.71.1 only supports TypeScript <6.1. Lift this when + # it does; until then a weekly PR would only fail the lint. + - dependency-name: typescript + versions: + - '>=7.0.0' - package-ecosystem: github-actions directory: / diff --git a/CHANGELOG.md b/CHANGELOG.md index 9111e9d..0cc4710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ entry per wave, newest first; the first six waves (V1–V6, the MVP) predate the are recorded in PLAN.md §B and §J. Each entry carries the opening sentence of its row and the reason the wave was built. +## V52.3 — TypeScript 7, measured and deferred + +The third major, and the one the plan flagged as uncertain. + +_Why:_ Approved 09/10/2026 with V52, option chosen by the owner after the measurement. A gate is not removed to make an upgrade fit; the upgrade waits for the gate. + ## V52.2 — vite-plugin-pwa 2, and a service worker that did not change by one byte of content The second of V52's three majors. @@ -18,6 +24,12 @@ The first of V52's three majors, each in its own pull request. _Why:_ Approved 09/10/2026 with V52. The safest of the three majors goes first: the suite it runs is also the instrument that proves it changed nothing. +## V52 — Major upgrades, one at a time, each measured + +Three majors, three pull requests, each with the before/after numbers that matter for it: Vitest 5 (V52.1), vite-plugin-pwa 2 (V52.2), TypeScript 7 (V52.3, measured and deferred). + +_Why:_ V44's rule: a major update keeps its migration cost visible. + ## V51 — A store that fits on a screen No behaviour change, and the proof is the suite: the 906 unit tests and the 97 Chromium e2e tests that passed before pass unchanged after. diff --git a/PLAN.md b/PLAN.md index c0b8546..8a670b4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -617,9 +617,12 @@ before features, and the one that produces wrong data comes first of all. | **V49 — delivered** | **CI that cannot be steered by a branch name.** The wave opened with zizmor 1.30.1 on the three workflows: **28 findings** (1 template injection, 21 actions pinned by a movable tag, 6 checkouts leaving the git token on the runner disk). **(1) Injection**: the deploy step expanded `github.head_ref` inside its script, so a branch named `x";…;"` would have run as code; the value now reaches the shell through `env:`. Low severity today (only branches of this repository deploy) and fixed before that stops being true. **(2) Pins**: every action is pinned to a full commit SHA with its release in a trailing comment (`# v7.0.1`), which Dependabot keeps current; a tag can be moved by whoever controls the action's repository, a SHA cannot. **(3) Credentials**: every checkout sets `persist-credentials: false`; no job pushes, so nothing needed the token on disk. After the fixes the offline audit reports **0 findings**, and a new `Workflow audit (zizmor)` job runs it on every pull request, installed from PyPI at a pinned version rather than through a third-party action, with the token the online audits need to check that each pinned SHA really belongs to its repository. Three new tests in `supply-chain.test.ts` hold the same rules per workflow file (SHA plus release comment on every `uses:`, no persisted credentials, no pull-request-controlled `${{ }}` inside a `run:`); they fail on the workflows before this wave. **(4) Coverage**: `npm run test:coverage` (V8 provider) replaces `npm run test` in CI with a floor set from the measured baseline rounded down, not a round number: **64.56 % statements, 53.28 % branches, 54.46 % functions, 65.62 % lines** on 898 unit tests, floors 64 / 53 / 54 / 65. The figures cover unit tests only; the interface is mostly exercised by the e2e suite, which this does not measure. Cost: the quality job grows by about a minute. **What this wave deliberately does not do**: chase a coverage target, add e2e coverage instrumentation, or gate deploys on the audit job. 898 unit tests, 123 e2e. | Approved 09/10/2026 after V48. Continues V44: a security gate is worth its maintenance only when it distinguishes a real exposure, and every rule here is now enforced by a test or a job rather than by a reviewer remembering it. | | **V50 — delivered** | **Metrics that agree with the reference on every edge.** Measured before anything changed, then checked against scikit-learn rather than against our own reading of it. **(1) Macro averages** ran over every class the task knows; scikit-learn averages over the labels present in `y_true` or `y_pred`. A perfect score on a batch holding two of three classes read **0.667** instead of **1.0**. Both implementations (`macroPrf` and the V45 decision policy) now follow scikit-learn, and the V42 generator gained an `absentClass` case (four classes declared, three present, one predicted but never true) that the old code failed. **(2) Rare classes**: the stratified split sent a class's only row to test, and the validation carve did the same to a two-row class, so the models never saw an example of either. Measured on 100 rows plus a rare class: 1 row gave test 1 / train 0, 2 rows gave test 1 / validation 1 / train 0. A single-row class now stays on the training side (1 → train 1; 2 → test 1, train 1; 3 and more unchanged, and the seeded draw of every other class is byte-identical). This is not hypothetical: three targets the demos offer have one-row classes (titanic → parch, cafe-sales → city, cafe-sales → product). On titanic → parch every model's macro F1 rises once the empty classes stop counting (GBDT 0.619 → 0.716, ensemble 0.422 → 0.548), and the decomposition shows the metric change alone moves nothing while every class is present: the gain comes from the split. The validation-picked winner moves from GBDT to the forest because the validation rows changed, not because a metric did. When every class has one row, nothing is left to test on and training refuses by name (`too-few-rows`) instead of reporting metrics over an empty set. **(3) Empty sets**: accuracy, log loss, RMSE, MAE, R² and the macro trio now return NaN on zero rows; before, RMSE 0, MAE 0 and R² 1 described a perfect model measured on nothing. No caller passes an empty set today (the test split keeps a row, batch scoring skips unlabeled files), so this is a contract for the next caller; the one theoretical path left, tuning on fewer than four rows, is noted rather than guarded. **What this wave deliberately does not do**: change any figure on a dataset whose classes all have three rows or more (every demo target used by a test or a tutorial), add a warning banner for rare classes, or guard k-fold tuning on tiny data. 906 unit tests, 123 e2e. | Approved 09/10/2026 after V49. The same rule as V35: a number must not flatter or punish by accident. Two of the three edges were reachable from the demo picker, and the reference that would have caught the first one existed since V42 but held no case with an absent class. | | **V51 — delivered** | **A store that fits on a screen.** No behaviour change, and the proof is the suite: the 906 unit tests and the 97 Chromium e2e tests that passed before pass unchanged after. **(1) `lab-store.ts`, 1 050 → 530 lines.** The 250-line `onmessage` chain becomes a dispatch table in `store/responses.ts`, one entry per `WorkerResponse` kind, typed as a mapped type so the compiler refuses a kind with no handler and a handler for a kind that does not exist. The chain's last `else` used to catch every unlisted kind as an error silently; that safety net is now a compile error instead. The bodies were moved by script, not retyped. The state shape and starting values go to `store/state.ts`, V47's stale-response fence to `store/fence.ts`, the threshold artifact builder to `store/artifacts.ts`; `lab-store.ts` keeps the actions and re-exports its public names, so no importer changed. Feature slices were considered and not done: the actions share one worker and one `send`, and slicing them would have moved code without separating anything. **(2) Tests named after waves** renamed after what they pin: `v35` → `honest-evaluation`, `v36` → `imbalance-ensemble`, `v37` → `knn-and-helpers`, `v38` → `french-numbers`, `v39` → `column-recipe`, `v40` → `quality-score`. The two mixed files (`v35`, `v36`) were renamed, not split, to keep the change mechanical. **(3) Duplicated helpers**: `numbersOf`, `missingRatio` and the binned shares, copied identically in `drift.ts` and `reference.ts`, now live once in `quality/distribution.ts`. The two quantile functions were left apart on purpose: on an empty list one returns 0 and the other NaN, and merging them would have been a behaviour change. **Measured side effect**: the coverage gate from V49 fired. The 35 branches became 35 functions, so the function denominator rose from 2 142 to 2 174 while covered functions barely moved, and the floor of 54 % failed at 53.72 %. Rather than lowering the floor, a table-driven test (`store/responses.test.ts`, 17 cases) now pins which fields each state-only answer writes, which is also the check a moved body needs; function coverage is back to 54.46 %. **What this wave deliberately does not do**: change a single handler body, split the mixed test files, or unify the quantile functions. 923 unit tests, 123 e2e. | Approved 09/10/2026 after V50. Only after V47 to V50, as the plan required: the store's concurrency rules and the metrics were pinned by tests before their code moved, so a regression in the move would have failed loudly. | -| **V52 — pending** | **Major upgrades, one at a time, each measured.** Vitest 5, vite-plugin-pwa 2, TypeScript 7. Each in its own pull request with the before/after numbers that matter for it (suite time, build time, service-worker behaviour offline). | V44's rule: a major update keeps its migration cost visible. | +| **V52 — delivered** | **Major upgrades, one at a time, each measured.** Three majors, three pull requests, each with the before/after numbers that matter for it: Vitest 5 (V52.1), vite-plugin-pwa 2 (V52.2), TypeScript 7 (V52.3, measured and deferred). Two shipped with nothing moved but rounding; the third passed on our code and was held back by the ecosystem, which is a finding, not a failure. | V44's rule: a major update keeps its migration cost visible. | | **V52.1 — delivered** | **Vitest 5, measured against Vitest 4 on the same commit.** The first of V52's three majors, each in its own pull request. The migration guide was read first and the repository searched for every pattern it names: no `toThrow('')`, no `sequential`, no `bench`, no removed `vitest/*` entrypoint, every `vi.mock` already at the top level of its file. Two changes could still have moved results silently, and both were measured rather than assumed. **`clearMocks` now defaults to `true`**: the 924 unit tests pass unchanged on the first run, so no test relied on calls accumulating across tests. **Coverage paths are now matched relative to the root**: the four figures move by rounding only (statements 64.93 → 64.92 %, lines 66.06 → 66.05 %, branches and functions identical; five statements fewer in the denominator), every V49 floor still met. Timing on the same machine stays inside its own noise: three coverage runs on Vitest 4 took 124 to 143 s, three on Vitest 5 131 to 134 s; plain runs 82 to 87 s on both. **Node**: Vitest 5 requires Node 22.12, so `engines` moves from `>=20` to `>=22.12` and the README says so; CI and `.nvmrc` were already on 22. **What this sub-wave deliberately does not do**: adopt Vitest 5's new options (`sharedViteServer`, artifact directory) or touch the other two majors. 924 unit tests, 123 e2e. | Approved 09/10/2026 with V52. The safest of the three majors goes first: the suite it runs is also the instrument that proves it changed nothing. | | **V52.2 — delivered** | **vite-plugin-pwa 2, and a service worker that did not change by one byte of content.** The second of V52's three majors. The 2.0.0 release notes list two consumer-facing changes: Node 20.19 as a floor (already covered by V52.1's 22.12) and `@vite-pwa/assets-generator` 2 accepted as a peer (not used here). A service worker breaks late and on someone else's machine, so the output was compared rather than trusted. Built on the same commit with 1.3.0 and 2.0.0: **98 precache entries, 5 675.73 KiB, in both**; the same 98 URLs; `sw.js` identical once revision hashes are masked; the same Workbox runtime file down to its content hash (`workbox-2ff6bd68.js`); the web manifest byte-identical. The offline e2e test (the whole lab working after the first visit, network cut) and the update-prompt unit tests pass unchanged. **What this sub-wave deliberately does not do**: change any PWA option. 924 unit tests, 123 e2e. | Approved 09/10/2026 with V52. The one upgrade of the three whose failure would reach visitors rather than developers, so its proof is the artifact visitors receive. | +| **V52.3 — delivered** | **TypeScript 7, measured and deferred.** The third major, and the one the plan flagged as uncertain. TypeScript 7 is the compiler rewritten in Go. Measured on the same commit, `tsc --noEmit` on the whole project: **TypeScript 6.0.3, 10.8 to 12.3 s over five runs; TypeScript 7.0.2, 1.5 to 1.7 s warm (3.9 s on a cold first run); 0 errors from either.** Our code is ready. The ecosystem is not: typescript-eslint 8.71.1, the latest release, declares `typescript >=4.8.4 <6.1.0`, and the lint is part of the CI gate. Two ways forward were weighed and the simpler one taken. Keeping TypeScript 6 for the lint and adding TypeScript 7 under an alias for the typecheck alone would save about ten seconds on a quality job of three and a half minutes, for the cost of two compilers to keep in step; not worth it. So TypeScript stays on 6, Dependabot ignores `typescript >=7.0.0` (a weekly pull request that can only fail the lint is noise, not a signal), and `supply-chain.test.ts` holds that rule like the others. **Resume condition**: typescript-eslint supports TypeScript 7; on our side the migration is then a version bump, since the code already passes. Recorded on the limits page as deferred after measurement, as « about seven times faster » rather than in seconds, since the docs quote no wall-clock duration. **What this sub-wave deliberately does not do**: disable or weaken the lint to make room for the faster compiler. 924 unit tests, 123 e2e. | Approved 09/10/2026 with V52, option chosen by the owner after the measurement. A gate is not removed to make an upgrade fit; the upgrade waits for the gate. | + +**Cap 7 closed (09/10/2026)**: V47 to V52 delivered. What the lab already did now holds when the user does two things at once (V47), when a link is damaged (V48), when CI runs on a branch nobody checked (V49), when a class is rare or absent (V50); the store fits on a screen (V51), and the three majors were taken one at a time, one of them deferred on a measurement rather than forced (V52). V12 (consented generative chat) remains pending a product decision. No wave starts without an explicit launch command. diff --git a/README.md b/README.md index a3a7641..5637889 100644 --- a/README.md +++ b/README.md @@ -342,10 +342,11 @@ 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). 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 +through the lab meeting the real world: real photos, real text, real file sizes). Cap 7 (closed) +made what already ships hold up under pressure: worker lifecycle, damaged share links, +CI hardening, metric edge cases, a store refactor, and major upgrades taken one at a time +(Vitest 5 and vite-plugin-pwa 2 shipped; TypeScript 7 typechecks this code with no error +in 1.5 s instead of about 11, and waits for typescript-eslint to support it). 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 21e7016..b1e9cc6 100644 --- a/src/content/docs/en/limits.md +++ b/src/content/docs/en/limits.md @@ -74,6 +74,13 @@ 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. +**TypeScript 7** (V52.3). Deferred, not dropped. The new compiler, rewritten +in Go, checks the whole project with not a single error, and about seven times +faster than TypeScript 6. But the tool that runs the lint does not accept +that version yet, and the lint is part of the CI gate. A check is not removed +to make an upgrade fit: the migration waits for the tool to support it, and on +our side it will then be a version bump. + ## Predictions measurement refuted This is the most useful category, and the most uncomfortable. diff --git a/src/content/docs/fr/limites.md b/src/content/docs/fr/limites.md index 8558f98..89ef071 100644 --- a/src/content/docs/fr/limites.md +++ b/src/content/docs/fr/limites.md @@ -76,6 +76,14 @@ 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. +**TypeScript 7** (V52.3). Reporté, pas abandonné. Le nouveau compilateur, +réécrit en Go, vérifie tout le projet sans une seule erreur, et environ sept +fois plus vite que TypeScript 6. Mais l'outil qui fait tourner le lint +n'accepte pas encore cette version, et le lint fait partie de la porte de la +CI. On ne retire pas une vérification pour faire entrer une mise à jour : la +migration attend que l'outil la supporte, et de notre côté elle se résumera +alors à changer un numéro de version. + ## 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/lib/supply-chain.test.ts b/src/lib/supply-chain.test.ts index 8d68edc..8fb11a3 100644 --- a/src/lib/supply-chain.test.ts +++ b/src/lib/supply-chain.test.ts @@ -47,6 +47,10 @@ describe('supply-chain automation', () => { 'dependency-name': '@huggingface/transformers', versions: ['>=4.3.0 <4.4.0'], }, + { + 'dependency-name': 'typescript', + versions: ['>=7.0.0'], + }, ], }, {