From 57818c402fa23e896875238da3cfe382e5d0c9a5 Mon Sep 17 00:00:00 2001 From: "Thierry V." <46031203+thierryvm@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:30:39 +0200 Subject: [PATCH 1/2] test(seo): guard llms.txt, llms-full.txt and index.html metadata against the curriculum The files that describe the curriculum to AI assistants and search engines had no test. A new guard compares them with curriculum.ts: module and lesson counts, module titles, and each lesson's title and URL, in order. Its first run found 10 lesson titles in llms-full.txt that had been renamed since (one GitHub lesson, nine AI lessons); they now match the curriculum. Mutating a count in each file turns the matching guard red. Phase 5d, lot 0b. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 ++ STORY.md | 10 +- docs/ROADMAP.md | 6 +- docs/plan.md | 6 +- .../curriculum-delivery-checklist.md | 2 +- public/llms-full.txt | 18 +-- src/test/publicCounters.test.ts | 106 ++++++++++++++++++ 7 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 src/test/publicCounters.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b14839b..a833a92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ --- +## 🤖 2 octobre 2026 — Ce que lisent les moteurs IA reste à jour + +*Fichiers `llms.txt` et `llms-full.txt` · métadonnées de la page d'accueil · Phase 5d, lot 0b* + +- Un nouveau test compare `llms.txt`, `llms-full.txt` et les métadonnées d'`index.html` au vrai curriculum : nombre de modules et de leçons, titre de chaque module, titre et adresse de chaque leçon. Un module ou une leçon ajoutés sans mettre ces fichiers à jour font maintenant échouer la CI. +- Dès son premier passage, il a trouvé **10 titres de leçon périmés** dans `llms-full.txt` (une leçon du module GitHub et neuf du module IA), renommées depuis. Ils sont corrigés. + +--- + ## 🪟 1er octobre 2026 — Sous Windows, `ls` répond comme PowerShell *Environnement Windows · `Get-ChildItem` · sorties vérifiées sur PowerShell 7.6* diff --git a/STORY.md b/STORY.md index 84ab34e..7977060 100644 --- a/STORY.md +++ b/STORY.md @@ -1267,6 +1267,14 @@ L'audit de fidélité a ensuite trouvé ce que j'avais laissé passer : sous Win Une capture ratée m'a aussi appris quelque chose : lancé depuis le dossier du projet, mon premier script a laissé quatre fichiers vides à sa racine. Ils n'ont pas atteint le dépôt, mais depuis, chaque capture tourne dans un dossier temporaire neuf. +### Le test qui a trouvé dix titres oubliés (2 octobre 2026) + +Avant d'ajouter la moindre commande, on a voulu que le site ne puisse plus mentir sur lui-même. Les assistants IA et les moteurs de recherche lisent deux fichiers, `llms.txt` et `llms-full.txt`, qui décrivent chaque module et chaque leçon. Aucun test ne les surveillait. + +Le garde-fou écrit ce matin compare ces fichiers au curriculum lui-même. Il a échoué dès son premier passage : dix leçons avaient été renommées depuis l'écriture de `llms-full.txt`, et le fichier donnait encore les anciens titres. Rien de grave, mais un assistant IA qui citait « Debug assisté par IA » renvoyait vers une leçon qui s'appelle autrement. + +On a aussi vérifié que le test sait échouer : en remettant volontairement un mauvais compteur dans chaque fichier, il passe au rouge à chaque fois. Les lots suivants ajouteront des modules et des leçons. Ces fichiers ne pourront plus rester en arrière. + --- ## Épilogue ouvert @@ -1282,4 +1290,4 @@ Ce journal continuera d'être écrit tant que le projet continue d'être constru --- *Terminal Learning est un projet open source, construit bénévolement en Belgique.* -*Dernière mise à jour : 1er octobre 2026 (le journal se lit désormais dans l'ordre, du prologue à aujourd'hui).* +*Dernière mise à jour : 2 octobre 2026.* diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a99d12f..45c7b65 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -2,6 +2,8 @@ > **Decision (1 October 2026, @thierry):** Terminal Learning becomes **the reference for learning the terminal from scratch up to an intermediate level**, within what a faithful simulation can do. Job-specific tools (offensive security, network analysis, OSINT) are not simulated; the site points to the free platforms that already teach them. Ordered plan: [Phase 5d](#phase-5d--from-the-basics-to-an-intermediate-level--planned-1-october-2026). Delivery checklist: [`docs/processes/curriculum-delivery-checklist.md`](processes/curriculum-delivery-checklist.md). > +> **Last update:** 2 October 2026 — **Phase 5d, Lot 0b** (#416): `llms.txt`, `llms-full.txt` and the `index.html` metadata are checked against the curriculum by a test; 10 stale lesson titles fixed. Next: Lot 1 (`sort`, `uniq`, `cut`). +> > **Previous update:** 1 October 2026 CEST — 🩺 **Full check-up (P1 terminal fidelity, THI-353), continued**: #396 every reference example runs · #397 tables, blank lines and mobile keys · #398 the installed app clears iOS 26's edge blur · #399 git reads real files, outputs match Git 2.56 · **#400 real git branches** (commit graph, ids identical to real git, merges and real conflicts; two fidelity audits, two code reviews) · #401 the landing page only lists commands the terminal runs and the lessons teach, and its terminal preview prints what real shells print. Planned: a **customising the terminal** module (idea from @thierry, #402). **Multi-step exercises checked on the terminal state** (the conflict lesson now resolves a real conflict end to end; no more auto-advance, a « Suivant » button instead). **Git module as steps** (init, first commit, diff, branch, merge). **Remote repositories as in real git** (`push`, `pull`, `fetch`, `clone`, `remote`, branch tracking in `status` and `branch -vv`, #406). **A remote where a colleague pushes** (`fetch`/`pull` bring real commits, a push is rejected with "fetch first", `gh` says it is not simulated; theory gaps 146 → 120). **GitHub module as steps** (remote, push & pull, fetch, pull request, merge strategies, GitHub Actions; #410, theory gaps 120 → 90). **The top of the installed iPhone app is clear at last** (#411, confirmed on @thierry's iPhone). **#413 Windows fidelity** (`ls`/`Get-ChildItem` print PowerShell 7.6's real table, `rm`/`cp`/`mv` are the cmdlets with their parameters — `rm -rf` is refused —, PowerShell's error messages, reference page updated; bash shown on Windows 34 → 32). Next: the ~20 commands taught but not simulated yet (`find`, `sort`, `diff`, `tar`, `Where-Object`…) and the remaining Windows debt (`New-Item`'s table, case-insensitive paths). > > **Previous update (26 September 2026 CEST)**: 🩺 **Full check-up (P1 terminal fidelity, THI-353)**: #389 a real shell layer (`;` `&&` `||`, pipelines, every redirection) — **198/198 exercises validate with no red line**; #390 output matches a real shell (`pwd`, `cd -`, `ls` in a pipe, `wc`, `apt`); #391 permanent replay of every lesson's theory (754 commands); #392 `cp`/`mv` into a directory (a `mv file .` wiped the home directory) and native Windows paths in PowerShell — theory gaps **174 → 162**, bash shown on Windows **39 → 37**; #393 audit agents refreshed (13 Opus / 8 Sonnet) + new `terminal-fidelity-auditor` (simulator vs real bash / PowerShell). Public landing roadmap corrected (LTI is a disabled spike, OpenAI BYOK blocked by CORS). Next: PowerShell error messages, `/app/reference` pedagogy, remaining theory gaps (Git history, `gh`, jobs). @@ -163,8 +165,8 @@ Full-stack developer path — 11 modules ✅ (66 lessons, 1035 unit tests) Full rationale and scope: [`docs/strategy/2026-10-01-bases-vers-intermediaire.md`](strategy/2026-10-01-bases-vers-intermediaire.md) (FR). One PR at a time, each following the [delivery checklist](processes/curriculum-delivery-checklist.md). Expected outputs come from real shells: Git Bash, PowerShell 7 (en-US) and Ubuntu under WSL 2. -- [ ] **Lot 0** — this plan, roadmap and delivery checklist -- [ ] **Lot 0b** — counter guards: `llms.txt`, `llms-full.txt` and `index.html` metadata checked by a test +- [x] **Lot 0** — this plan, roadmap and delivery checklist (#415) +- [x] **Lot 0b** — counter guards: `llms.txt`, `llms-full.txt` and `index.html` metadata checked by a test (`src/test/publicCounters.test.ts`, #416); 10 stale lesson titles fixed in `llms-full.txt` - [ ] **Lot 1** — search and sort: `sort`, `uniq`, `cut`, `find`, `xargs`, `diff`, useful `wc`/`grep` options; PowerShell `Sort-Object`, `Where-Object`, `Select-Object` (3–4 PRs) - [ ] **Lot 2** — archives and disk: `tar`, `gzip`, `zip`/`unzip`, `Compress-Archive`, `du`, `df`, `which`/`type`/`Get-Command` (1–2 PRs) - [ ] **Lot 3** — `sed` and `awk`, the subset a beginner uses, with an honest note beyond it (1–2 PRs) diff --git a/docs/plan.md b/docs/plan.md index ce3b923..330c8cf 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -2,6 +2,8 @@ > **Décision du 1er octobre 2026 (@thierry)** : Terminal Learning devient **la référence pour apprendre le terminal de zéro jusqu'au niveau intermédiaire**, dans les limites d'une simulation fidèle. Les outils de métier (sécurité offensive, analyse réseau, OSINT) ne sont pas simulés : le site oriente vers les plateformes gratuites qui les enseignent. Plan ordonné : [`docs/strategy/2026-10-01-bases-vers-intermediaire.md`](strategy/2026-10-01-bases-vers-intermediaire.md) · procédure de livraison : [`docs/processes/curriculum-delivery-checklist.md`](processes/curriculum-delivery-checklist.md). > +> Dernière mise à jour : **2 octobre 2026** — **Phase 5d, lot 0b** (#416) : `llms.txt`, `llms-full.txt` et les métadonnées d'`index.html` sont comparés au curriculum par un test ; 10 titres de leçon périmés corrigés. Suite : lot 1 (`sort`, `uniq`, `cut`). +> > Précédente mise à jour : **1er octobre 2026 CEST** — **🩺 Grand Check-up, P1 « fidélité du terminal » (THI-353), suite** : #396 exemples de la Référence tous exécutables · #397 tableaux, lignes vides et touches du clavier mobile · #398 haut de l'app installée dégagé du flou d'iOS 26 · #399 git lit les vrais fichiers, sorties identiques à Git 2.56 · **#400 vraies branches Git** (graphe de commits, identifiants identiques au vrai Git, fusions et vrais conflits ; deux audits de fidélité et deux revues de code) · #401 page d'accueil : ne promet plus que des commandes simulées · #402 module « Personnaliser son terminal » annoncé · **exercices en plusieurs étapes vérifiés sur l'état du terminal** (le conflit Git se résout de A à Z, plus de saut automatique : bouton « Suivant »). · **module Git en étapes** (init, premier commit, diff, branche, fusion) · **dépôts distants identiques au vrai Git** (`push`, `pull`, `fetch`, `clone`, `remote`, suivi de branche dans `status` et `branch -vv`, #406) · **dépôt distant où un collègue pousse** (`fetch`/`pull` ramènent de vrais commits, push refusé « fetch first », `gh` dit qu'il n'est pas simulé ; écarts théorie 146 → 120) · **module GitHub en étapes** (remote, push & pull, fetch, Pull Request, stratégies de merge, GitHub Actions ; #410, écarts théorie 120 → 90) · **haut de l'app installée sur iPhone enfin net** (#411, confirmé sur l'iPhone de @thierry). **#413 fidélité Windows** (`ls`/`Get-ChildItem` affichent le vrai tableau de PowerShell 7.6, `rm`/`cp`/`mv` sont les cmdlets avec leurs paramètres — `rm -rf` refusé —, erreurs de PowerShell, page Référence à jour ; bash montré sous Windows 34 → 32). Suite : les ~20 commandes enseignées mais pas encore simulées (`find`, `sort`, `diff`, `tar`, `Where-Object`…) et la dette Windows restante (tableau de `New-Item`, chemins insensibles à la casse). > Précédent : **26 septembre 2026 CEST** — **🩺 Grand Check-up, P1 « fidélité du terminal » (THI-353)** : #389 vraie couche shell (**198/198 exercices sans ligne rouge**) · #390 sorties d'un vrai shell · #391 rejeu permanent de la théorie · #392 `cp`/`mv` vers un dossier + chemins Windows natifs (écarts théorie **174 → 162**) · #393 agents d'audit remis à jour + `terminal-fidelity-auditor` · roadmap publique corrigée. Suite : messages d'erreur PowerShell, page Référence, écarts restants. > Statut global : **Phase 5 EN COURS** — Curriculum Expansion : 11 modules ✅, 66 leçons, **2500+ tests unitaires** + 176 E2E — **Vision consolidée** : LTI-first (ADR-001), BYOK OpenRouter 4-tiers (ADR-002), TTFR KPI central (ADR-003), Classroom Composer UI (ADR-004), AI Tutor V1 décisions gelées (ADR-005 — stockage, rate-limit, guardrails), Solo-sustainable practices (ADR-006), tuteur IA socratique dès A1, i18n FR/NL/EN — Architecture stratégique précédente (THI-35) : Terminal Sentinel (Phase 5.5) ✅, RBAC complet (Phase 7) ✅, Admin Panel (Phase 9), PWA avancée (Phase finale) — **Epic Web 2026 Compliance** (THI-96) : 6/8 sub-issues livrées (THI-97 → THI-102), reste Desktop a11y + CSS moderne 2026 — **Phase 7b (AI Tutor V1) ✅ COMPLETE + V1.5 SÉQUENCÉE** : THI-115 ✅, THI-109 ✅ (gate-zero guardrail), THI-110 ✅ (keyManager AES-GCM), THI-120 ✅ (Sentry scrubber), **THI-111 ✅ COEUR FONCTIONNEL** (PR #188 — sanitizer + 4 providers + panel + 287 AI tests, audits guardrail 9.4/10 + security 8.8/10 + ui A11y exemplary), **THI-147 ✅ FIX SAFE-AREA iPhone PWA** (PR #189), **panel actif en Production** (`VITE_AI_TUTOR_ENABLED=true` + `VITE_AI_TUTOR_OPENROUTER_MODEL=anthropic/claude-haiku-4-5` activés Production+Preview par @cowork) — **Sprint sécurité 1-2 mai 2026 ✅ CLOS** : audit security-auditor 8.1/10 → ~8.6/10 post-sprint, 11 PRs livrées (#168 à #178), 5 HIGH/MEDIUM Done (THI-133/134/135/137/140), 4 MEDIUM ciblés en backlog (THI-136/138/139/112), agent `route-attack-auditor` créé — **Session 4 mai 2026** : THI-111 livré + THI-147 livré + 5 tickets V1.5 backlog créés (THI-142 lessonContext renforcé HIGH, THI-143 frustration heuristic + détection sémantique user MEDIUM, THI-144 system prompt v1.1.0 + ADR-008 + eval suite MEDIUM, THI-145 chat assistant role-based Phase 9+ LOW, THI-146 modèle défaut Haiku HIGH déjà actif via env var, THI-148 extend tutor scope platform meta-questions V1.0.1 P1 1h30 estimé) — **Posture validée** : pas de rush deadline, qualité/scalabilité/perf non négociables, plan respecté en ordre, **mea culpa explicite** à chaque round trio @thierry / @cc-terminallearning / @cowork (estimation 30 min → 1h30, privacy `userProgress` retirée V1.0.1, hypothèse `transform` mobile réfutée par diagnostic Chrome DevTools MCP) — **Verdict empirique Haiku 4.5 (capturé 4 mai 21h par @cowork via Chrome MCP)** : 5 tests qualitatifs, score moyen **9.3/10** — Test 1 méta-plateforme 8/10, Test 2 fichiers cachés 9/10, Test 3 hallucination 9.5/10, Test 4 frustration 10/10 (Haiku bascule mode direct AUTONOMEMENT), Test 5 jailbreak 10/10. **Reprio backlog tranchée @cowork** : THI-146 ✅ SUCCÈS validé, **THI-142 → Low** (Haiku gère déjà bien le contexte leçon), **THI-143 → Low** (Haiku résout naturellement la frustration via compréhension contextuelle), **THI-148 P1 INCHANGÉ** (scope ≠ modèle prouvé Test 1, GO IMMÉDIAT), THI-144 P2 Medium (peut englober THI-148 + eval suite), THI-145 P3 Low (Phase 9+). **ROI méthode scientifique** : ~4-6h économisées (THI-142/143 reportés V2). **Next ordonné — décision @cowork 5 mai matin** : (1) **THI-150 EN COURS** (12ᵉ agent `mobile-responsive-auditor`, ex-brick 3a de THI-149 epic Done, 11 sections / ≥48 checkpoints + bonus Section 11 Desktop Preservation + checkpoints BUG-FAB-001 visibility/contrast/detachment), (2) THI-151 audit Playwright WebKit + matrice bugs (ex-brick 3b), (3) THI-152 mini-PRs fix séquentielles (ex-brick 3c, critère ABSOLU **ne pas casser desktop**), (4) THI-148 extend tutor scope méta-plateforme V1.0.1 (1h30 honnête, scope statique platformContext, bump v1.0.0→v1.0.1, audit guardrail Règle 10 obligatoire), (5) **THI-144 enrichi P1** system prompt v1.1.0 + ADR-008 + eval suite (intègre 5 micro-frictions identifiées par cross-validation ChatGPT sur session 8 tours @thierry — compound questions, sur-explication internal mechanics, indices répétés, platformContext absent confirme THI-148, conclusion ouverte), (6) THI-112 onboarding AiKeySetup + picker modèle curated, (7) THI-114 Web Worker isolation, (8) THI-145 chat role-based Phase 9+. **THI-142/143 reportés V2** (Haiku 9.3/10 résout 80% naturellement). @@ -249,8 +251,8 @@ Page `/privacy` créée. Vercel Analytics sans cookies → pas de bannière cook Détail, périmètre et justification : [`docs/strategy/2026-10-01-bases-vers-intermediaire.md`](strategy/2026-10-01-bases-vers-intermediaire.md). Une PR à la fois, chacune suit la [liste de contrôle de livraison](processes/curriculum-delivery-checklist.md). -- [ ] **Lot 0** — plan, roadmap, procédure de livraison -- [ ] **Lot 0b** — garde-fous des compteurs (`llms.txt`, `llms-full.txt`, métadonnées d'`index.html`) +- [x] **Lot 0** — plan, roadmap, procédure de livraison (#415) +- [x] **Lot 0b** — garde-fous des compteurs (`llms.txt`, `llms-full.txt`, métadonnées d'`index.html`) : `src/test/publicCounters.test.ts` (#416), 10 titres périmés corrigés dans `llms-full.txt` - [ ] **Lot 1** — chercher et trier : `sort`, `uniq`, `cut`, `find`, `xargs`, `diff` ; `Sort-Object`, `Where-Object`, `Select-Object` - [ ] **Lot 2** — archives et disque : `tar`, `gzip`, `zip`, `Compress-Archive`, `du`, `df`, `which`/`Get-Command` - [ ] **Lot 3** — `sed` et `awk` (sous-ensemble débutant) diff --git a/docs/processes/curriculum-delivery-checklist.md b/docs/processes/curriculum-delivery-checklist.md index dd30fe7..6b9ef74 100644 --- a/docs/processes/curriculum-delivery-checklist.md +++ b/docs/processes/curriculum-delivery-checklist.md @@ -39,7 +39,7 @@ - [ ] `index.html` : métadonnées (description, Open Graph, Twitter) et FAQ en JSON-LD. - [ ] `public/llms.txt` et `public/llms-full.txt`. - [ ] `README.md`, `CLAUDE.md` (section Phases), `docs/ARCHITECTURE.md`, `docs/exports/README.md`. -- [ ] Les tests `docLessonCount`, `landingTotals` et `seo` doivent passer. Un échec signale un compteur à corriger partout, pas un test à ajuster. +- [ ] Les tests `docLessonCount`, `landingTotals`, `publicCounters` (`llms*.txt` et métadonnées d'`index.html`, depuis le 2 octobre 2026) et `seo` doivent passer. Un échec signale un compteur à corriger partout, pas un test à ajuster. ## 3. Avant la PR diff --git a/public/llms-full.txt b/public/llms-full.txt index 9ca9f2f..a1ee682 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -161,7 +161,7 @@ Terminal Learning is a free, open-source, browser-based interactive platform to | git push & pull — Synchroniser avec GitHub | `git push`, `git pull`, `git push -u origin` | /app/learn/github-collaboration/git-push-pull | | git fetch & clone — Récupérer et cloner | `git fetch`, `git clone` | /app/learn/github-collaboration/git-fetch-clone | | Pull Requests — Code Review & Collaboration | GitHub PR workflow, review, approvals | /app/learn/github-collaboration/pull-requests | -| Merge strategies — merge vs rebase vs squash | fast-forward, merge commits, `git rebase`, squash strategy | /app/learn/github-collaboration/merge-strategies | +| Stratégies de merge — Merge commit, Squash, Rebase | fast-forward, merge commits, `git rebase`, squash strategy | /app/learn/github-collaboration/merge-strategies | | Conflits de merge — Les résoudre comme un pro | conflict markers, `git mergetool`, resolution strategies | /app/learn/github-collaboration/conflicts | | GitHub Actions — CI/CD automatisé | `.github/workflows/`, YAML syntax, triggers, jobs | /app/learn/github-collaboration/github-actions | @@ -177,14 +177,14 @@ Terminal Learning is a free, open-source, browser-based interactive platform to | Ce que l'IA sait vraiment faire | `ai-help capabilities` | /app/learn/ia-dev/ia-dev-capacites | | Ce que l'IA ne sait pas faire | `ai-help limits` | /app/learn/ia-dev/ia-dev-limites | | Écrire un bon prompt — les bases | `ai-help prompts` | /app/learn/ia-dev/ia-dev-prompts-basics | -| Prompts avancés — contexte technique | `ai-help context` | /app/learn/ia-dev/ia-dev-prompts-avances | -| Valider la sortie de l'IA | `ai-help validate` | /app/learn/ia-dev/ia-dev-valider | -| Debug assisté par IA | `ai-help debug` | /app/learn/ia-dev/ia-dev-debug | -| Sécurité et code IA | `ai-help security` | /app/learn/ia-dev/ia-dev-securite | -| Claude CLI dans le terminal | `ai-help claude-cli` | /app/learn/ia-dev/ia-dev-claude-cli | -| L'IA dans différents métiers | `ai-help professions` | /app/learn/ia-dev/ia-dev-metiers | -| La posture professionnelle face à l'IA | `ai-help posture` | /app/learn/ia-dev/ia-dev-posture | -| Workflow complet avec l'IA | `ai-help workflow` | /app/learn/ia-dev/ia-dev-workflow | +| Prompts avancés — donner du contexte technique | `ai-help context` | /app/learn/ia-dev/ia-dev-prompts-avances | +| Valider la sortie — ne jamais copier-coller aveuglément | `ai-help validate` | /app/learn/ia-dev/ia-dev-valider | +| Déboguer avec l'IA — workflow pratique | `ai-help debug` | /app/learn/ia-dev/ia-dev-debug | +| Sécurité & IA — ce qu'il ne faut jamais partager | `ai-help security` | /app/learn/ia-dev/ia-dev-securite | +| Claude Code CLI — l'IA dans le terminal | `ai-help claude-cli` | /app/learn/ia-dev/ia-dev-claude-cli | +| L'IA par parcours métier | `ai-help professions` | /app/learn/ia-dev/ia-dev-metiers | +| Posture senior : l'IA comme amplificateur | `ai-help posture` | /app/learn/ia-dev/ia-dev-posture | +| Workflow complet — du brief au déploiement avec l'IA | `ai-help workflow` | /app/learn/ia-dev/ia-dev-workflow | --- diff --git a/src/test/publicCounters.test.ts b/src/test/publicCounters.test.ts new file mode 100644 index 0000000..8dab3cb --- /dev/null +++ b/src/test/publicCounters.test.ts @@ -0,0 +1,106 @@ +/// +/** + * Guard: the public files that describe the curriculum to search engines and + * AI assistants stay in step with the real curriculum. + * + * `landingTotals.test.ts` already ties the landing constants and the FAQ + * sentences of `index.html` to the curriculum. Until 2 October 2026 nothing + * checked `public/llms.txt`, `public/llms-full.txt` or the "11 modules" in the + * `index.html` metadata, so a new module or lesson would have left them behind + * in silence (Phase 5d, lot 0b). + * + * When this test fails, update the public file, never the test: it reads the + * expected values from `curriculum.ts`. + */ + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, it, expect } from 'vitest'; +import { curriculum, getTotalLessons } from '../app/data/curriculum'; + +const read = (relPath: string) => readFileSync(resolve(process.cwd(), relPath), 'utf-8'); +const lessonPath = (moduleId: string, lessonId: string) => `/app/learn/${moduleId}/${lessonId}`; + +/** Splits a file on its "### Module N — Title" headings, in order. */ +function moduleSections(content: string) { + const heading = /^### Module (\d+) — (.+)$/gm; + const found = [...content.matchAll(heading)]; + return found.map((m, i) => ({ + number: Number(m[1]), + heading: m[2].trim(), + body: content.slice(m.index! + m[0].length, found[i + 1]?.index ?? content.length), + })); +} + +describe('public/llms.txt follows the curriculum', () => { + const content = read('public/llms.txt'); + const sections = moduleSections(content); + + it('states the real number of modules and lessons', () => { + const m = content.match(/## Curriculum \((\d+) modules, (\d+) lessons\)/); + expect(m, 'the "## Curriculum (N modules, M lessons)" heading was not found').not.toBeNull(); + expect(Number(m![1])).toBe(curriculum.length); + expect(Number(m![2])).toBe(getTotalLessons()); + }); + + it('has one section per module, in curriculum order, with the module title', () => { + expect(sections.map((s) => s.number)).toEqual(curriculum.map((_, i) => i + 1)); + expect(sections.map((s) => s.heading)).toEqual(curriculum.map((m) => m.title)); + }); + + it('lists every lesson URL of each module, in order, and nothing else', () => { + curriculum.forEach((mod, i) => { + const urls = [...sections[i].body.matchAll(/^- https:\/\/terminallearning\.dev(\/app\/learn\/\S+)$/gm)].map((u) => u[1]); + expect(urls, `llms.txt, module ${i + 1} (${mod.id})`).toEqual(mod.lessons.map((l) => lessonPath(mod.id, l.id))); + }); + }); +}); + +describe('public/llms-full.txt follows the curriculum', () => { + const content = read('public/llms-full.txt'); + const sections = moduleSections(content); + + it('states the real number of modules and lessons', () => { + const m = content.match(/(\d+) modules, (\d+) lessons total/); + expect(m, 'the "N modules, M lessons total" sentence was not found').not.toBeNull(); + expect(Number(m![1])).toBe(curriculum.length); + expect(Number(m![2])).toBe(getTotalLessons()); + }); + + it('has one section per module with its title and lesson count', () => { + expect(sections.map((s) => s.number)).toEqual(curriculum.map((_, i) => i + 1)); + expect(sections.map((s) => s.heading)).toEqual( + curriculum.map((m) => `${m.title} (${m.lessons.length} lessons)`), + ); + }); + + it('has a table row for every lesson of each module, with its title and URL, in order', () => { + curriculum.forEach((mod, i) => { + const rows = [...sections[i].body.matchAll(/^\| (.+?) \| .* \| (\/app\/learn\/\S+) \|$/gm)].map((r) => ({ + // A "|" inside a Markdown table cell is written "\|". + title: r[1].trim().replace(/\\\|/g, '|'), + url: r[2], + })); + expect(rows, `llms-full.txt, module ${i + 1} (${mod.id})`).toEqual( + mod.lessons.map((l) => ({ title: l.title, url: lessonPath(mod.id, l.id) })), + ); + }); + }); +}); + +describe('index.html metadata follows the curriculum', () => { + const html = read('index.html'); + + it('every "N modules" is the real number of modules', () => { + const counts = [...html.matchAll(/(\d+) modules/g)].map((m) => Number(m[1])); + // Meta description, Open Graph, Twitter, two JSON-LD descriptions and the FAQ. + expect(counts.length).toBeGreaterThanOrEqual(6); + expect(counts.every((n) => n === curriculum.length), `found ${counts.join(', ')}`).toBe(true); + }); + + it('every "N leçons" is the real number of lessons', () => { + const counts = [...html.matchAll(/(\d+) leçons/g)].map((m) => Number(m[1])); + expect(counts.length).toBeGreaterThanOrEqual(1); + expect(counts.every((n) => n === getTotalLessons()), `found ${counts.join(', ')}`).toBe(true); + }); +}); From 66cc28e2182a0a08cdd5cab57ba0507e807031e5 Mon Sep 17 00:00:00 2001 From: "Thierry V." <46031203+thierryvm@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:31:51 +0200 Subject: [PATCH 2/2] test(seo): name the missing module heading instead of a TypeError Co-Authored-By: Claude Opus 5.5 --- src/test/publicCounters.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/publicCounters.test.ts b/src/test/publicCounters.test.ts index 8dab3cb..d53aa37 100644 --- a/src/test/publicCounters.test.ts +++ b/src/test/publicCounters.test.ts @@ -50,6 +50,7 @@ describe('public/llms.txt follows the curriculum', () => { it('lists every lesson URL of each module, in order, and nothing else', () => { curriculum.forEach((mod, i) => { + expect(sections[i], `llms.txt has no "### Module ${i + 1}" heading`).toBeDefined(); const urls = [...sections[i].body.matchAll(/^- https:\/\/terminallearning\.dev(\/app\/learn\/\S+)$/gm)].map((u) => u[1]); expect(urls, `llms.txt, module ${i + 1} (${mod.id})`).toEqual(mod.lessons.map((l) => lessonPath(mod.id, l.id))); }); @@ -76,6 +77,7 @@ describe('public/llms-full.txt follows the curriculum', () => { it('has a table row for every lesson of each module, with its title and URL, in order', () => { curriculum.forEach((mod, i) => { + expect(sections[i], `llms-full.txt has no "### Module ${i + 1}" heading`).toBeDefined(); const rows = [...sections[i].body.matchAll(/^\| (.+?) \| .* \| (\/app\/learn\/\S+) \|$/gm)].map((r) => ({ // A "|" inside a Markdown table cell is written "\|". title: r[1].trim().replace(/\\\|/g, '|'),