Rendre l'alignement des façades vérifiable par le build plutôt que par relecture, et refermer les vingt-trois écarts que l'audit d'août 2026 a confirmés entre le cœur, le CLI, JS, WASI, la façade JVM/Java et la démo.
L'objectif n'est pas un générateur. C'est un catalogue d'options unique qui vérifie avant de générer, plus une règle de test pour la seule classe de défaut qu'aucun catalogue ne voit : une clé qu'un lecteur accepte et qu'un export n'utilise pas.
docs/ledgers/surface-coverage.md— le tableau d'état, source des écarts listés iciengine/src/jvmTest/…/wasi/WasiParityTableTest.kt— l'idiome de test réutilisé (un test JVM qui lit les autres source sets comme du texte):codegen— le module qui hébergera le catalogue
Les trois familles de défauts confirmés, par ordre d'impact utilisateur.
| # | Défaut | Où |
|---|---|---|
| F1 | vcWriteGpxTracks parse trackName et startTimeEpochMs puis ne les transmet pas ; l'hôte reçoit un succès et un GPX aux traces sans nom, aux temps relatifs à l'époque 0 — et l'ABI les documente comme fonctionnelles |
EngineWasiApi.kt:747 |
| F2 | La démo détruit tous les <wpt> sur un aller-retour chargement → simulation → téléchargement (parseGpx ne porte pas les waypoints, writeGpxAt n'a pas de paramètre waypoints) |
GpxAnalysisView.vue:93, useGPXDemo.ts:154 |
| F3 | La démo jette tout <trk> après le premier (parseGpx = firstTrackAsPath) |
useGPXDemo.ts:154 |
| F4 | Une résolution de ligne de course non convergée est tracée sur la carte sans avertissement : report.converged n'est jamais lu |
useMap.ts:289 |
| F5 | Préséance roadCondition inversée entre le CLI et JS/WASI : maxLeanAngleDeg = 42 + roadCondition = "wet" donne 42° d'un côté, ~15,6° de l'autre |
CyclistMixin.kt:136,139 vs WasiOptions.kt:145,148 et EngineJsApi.kt:565,568 |
- Java (l'angle mort structurel) : pas de fabrique pour
CyclistPowerSpec,CoursePhysicsniCourse;ElevationProvider.getElevationsAlongn'a aucun pontBlocking/Async. - CLI : aucune porte pour la détection de cols ; aucune option
CsvOptions/JsonOptions; aucun accès àElevationProviderConfig; pas d'interPathGapsur les écritures FIT ; pas de filtretracksAsPaths(kinds); pas de--no-gpx-repair. - JS :
ClimbOptions.maxAnalysisPointsabsent ;pathToCsva trois paramètres etpathToJsondeux, doncdecimals,includeMetaetfieldssont hors d'atteinte alors que WASI les accepte. - WASI :
maxAnalysisPointsrejeté comme clé inconnue ;fieldsetlineSeparatorrejetés eux aussi (ce n'est pas un oubli silencieux, c'est un refus dur). - Démo : dix des trente
@JsExportd'EngineJsApine sont pas liés dansengine-shim.ts, dontparseGpxTracks,writeGpxTracks,pathToCsv,pathToJson,pathsToFit. - Hôte de référence :
vcAnalyzeRacingLineJsonest le seul@WasmExportsans aide nommée danstools/wasi/host.pyni assertion danstest_engine.py.
ENHANCE_OPTIONS_KEYS(jsMain,private) n'a aucun lien compilateur avecEnhanceOptionsDto.- Les défauts réécrits en littéraux :
EngineJsApi.kt:508-517,ClimbDetectorJvm.kt:23-31,EngineModelJvm.simplifyPathOptions/curvatureOptions. ElevationJsApin'a aucunrequireOnlyKeys: une clé mal orthographiée y est ignorée.RoadConditionest le seul enum inter-portes sans catalogue fil danscommonMain— cause directe de F5.
Chaque étape est livrable seule. L'ordre est « le moins cher et le plus utile d'abord » ; les étapes S0 à S2 ne dépendent d'aucun mécanisme neuf.
Pointer la regex de WasiParityTableTest sur tools/wasi/host.py en plus de
WasiExportCatalog.kt, pour que tout @WasmExport doive avoir une aide dans l'hôte de référence ;
ajouter l'assertion vcAnalyzeRacingLineJson dans tools/wasi/test_engine.py. Dans le même commit,
déclarer les inputs.files(…) / inputs.dir("demo/src") sur :engine:jvmTest et :cli:test —
sans quoi une tâche de test qui lit d'autres source sets reste UP-TO-DATE sur exactement les
modifications qu'elle est censée surveiller.
Extraire un internal fun writeGpxTracksText(paths, options) hors de vcWriteGpxTracks, transmettre
options.trackName et options.startTimeEpochMs, et l'épingler dans un wasmWasiTest sous l'export
(idiome w03 : le runner KGP ne sait pas fournir read_input). Ajouter trackNames /
startTimeEpochMs au writeGpxTracks JS. Corriger wasm-wasi-abi.md §9 et §11.
C'est le défaut le plus grave, et aucun mécanisme statique ne l'aurait jamais attrapé.
Livré : writeGpxTracksText transmet les quatre clés, quatre wasmWasiTest l'épinglent (dont deux
qui échouent sur l'ancien corps), writeGpxTracks côté JS prend trackNames et startTimeEpochMs
avec deux jsNodeTest, et l'hôte de référence vérifie les deux clés à travers l'ABI. Les formes
divergent volontairement : JS prend un tableau positionnel, WASI une chaîne pour toutes les traces.
Reste ouvert : la démo ne propose toujours pas d'export multi-traces (F3, étape S2).
Lier les dix @JsExport manquants dans engine-shim.ts ; parser via parseGpxTracks pour qu'un
second <trk> survive ; garder les waypoints à côté du Path pour que le téléchargement cesse de
les détruire. Ajouter les téléchargements CSV/JSON dans Toolbar.vue pendant que le shim est ouvert.
Supprimer ou justifier les cinq réexports morts (enhance, writeGpx, pointAt,
dominantHeadwindAzimuth, detectClimbsWithOptions) et le PointDto mort.
Livré : F2 et F3 sont refermés. Le shim lie les trente @JsExport (dix de plus), la démo parse par
parseGpxTracks avec un sélecteur de trace dans la barre d'outils, garde les waypoints à côté du
Path et exporte par writeGpxTracks, et le menu Download gagne CSV et JSON. Les onze bindings
que aucun contrôle n'atteint sont listés nommément en bas d'engine-shim.ts, chacun avec sa
raison — un réexport n'est pas une traversée de surface, et la liste doit rester lisible comme
telle. Ajouté demo/public/gpx/two-tracks.gpx : aucun échantillon livré n'avait plus d'une trace
ni le moindre waypoint, ce qui est précisément pourquoi les deux pertes ont survécu si longtemps.
Vérifié dans le navigateur, pas seulement au typecheck.
Reste ouvert : pathsToFit (FIT multi-tours) n'a pas de contrôle, et les options de CsvOptions /
JsonOptions non plus — elles n'atteignent pas encore la porte JS (étape S4).
engine/src/jvmTest/…/surface/DoorKeyParityTest.kt : égalité à quatre voies entre les propriétés de
l'external interface Kotlin, ENHANCE_OPTIONS_KEYS, ENHANCE_KEYS et le miroir TypeScript, pour
les cinq DTO partagés (14/7/6/2/7 noms, identiques aujourd'hui). Réutiliser
DocumentedFieldCountTest.repositoryRoot(). Chaque extracteur porte un auto-contrôle
assertTrue(found.size > n), pour qu'une regex cassée échoue bruyamment au lieu de rétrécir en
silence.
Le livrer vert : le mécanisme est relu pour lui-même avant d'être attaché à une assertion rouge.
C'est le test qui aurait attrapé la fusion 43/44.
Livré, et vert : les quatre portes s'accordent aujourd'hui sur les cinq DTO (14/7/6/2/7 noms). Quatre assertions — l'allowlist JS contre son propre DTO, les deux portes fil l'une contre l'autre, le miroir TypeScript contre le Kotlin, plus l'auto-contrôle de taille par extracteur.
Non vacuité vérifiée en cassant chaque porte à son tour : une clé ajoutée au seul miroir TS, une
clé retirée du seul lecteur WASI, une propriété ajoutée à la seule external interface (le bug
43/44 reproduit), et une taille attendue fausse. Les deux premières n'ont d'abord rien cassé :
la liste inputs.files de S0 ne couvrait ni WasiOptions.kt ni engine-shim.ts, donc la tâche
restait UP-TO-DATE. Corrigé dans le même commit — et c'est la démonstration la plus nette qui soit
que la déclaration d'entrées n'est pas de l'hygiène de build mais la condition d'existence du test.
codegen/src/main/kotlin/io/github/glandais/codegen/surface/OptionCatalog.kt : la déclaration
unique, écrite à la main, qui tient des symboles réels et non des chaînes. Chaque entrée nomme
une KClass et un chemin d'accès pointé dans l'objet instancié —
Opt("simplifyToleranceM", path = "simplifyPath.toleranceM") — jamais un littéral, si bien qu'un
vérificateur ne peut épeler que d.simplifyPath.toleranceM.
La complétude est dérivée, pas déclarée : EnhanceOptions::class.primaryConstructor!!.parameters,
récursivement dans les classes d'options imbriquées, énumère le vrai jeu de champs, et chaque
paramètre doit apparaître soit comme Opt, soit comme CoreOnly(reason).
C'est la décision de conception non négociable. Le dépôt a déjà commis l'erreur inverse :
GeneratePath.FIELDSest un miroir recopié à la main dePointField.kt, gardé par le seulEXPECTED_COUNT = 43. Un catalogue qui redit les noms de champs sous forme de chaînes estFIELDSavec plus de surface, et donc négatif.
Le catalogue vit dans :codegen et pas dans commonMain pour deux raisons portantes :
engine/build.gradle.kts enregistre checkWasmModuleSize, donc ~40 descripteurs avec leurs textes
d'aide sont du poids mort dans le .wasm ; et un catalogue en commonMain ne peut pas réfléchir.
Commencer par ClimbOptions, CsvOptions et JsonOptions seulement — les trois plus petites, et
les trois qui ont des écarts vivants. Ajouter DoorArityTest et DoorDefaultsTest. Attendre du
rouge sur maxAnalysisPoints, CsvOptions.decimals/fields/lineSeparator,
JsonOptions.decimals/includeMeta/fields. Le verdir en élargissant JS et WASI (et en donnant au
CLI au moins --csv-fields / --decimals), ou en écrivant des raisons CoreOnly.
S'arrêter ici et réévaluer avant d'écrire le moindre émetteur : cette étape est peut-être tout le livrable qui en vaut la peine.
Le catalogue est bien rouge au premier lancement, et sur exactement les cinq écarts prédits :
DoorParityTest > every option the catalog gives a JS door has one() FAILED
[ClimbOptions.detectClimbsWithOptions is missing [maxAnalysisPoints],
CsvOptions.pathToCsv is missing [decimals],
JsonOptions.pathToJson is missing [decimals, includeMeta]]
DoorParityTest > every option the catalog gives a WASI door has one() FAILED
[CLIMB_KEYS is missing [maxAnalysisPoints], CSV_KEYS is missing [lineSeparator]]
Verdi en élargissant les deux portes fil, avec un test par porte, plus l'hôte de référence.
fields reste CoreOnly avec sa raison : aucune porte ne sait épeler un PointField sur le fil.
Trois leçons, dont deux sur les tests eux-mêmes :
- Les assertions accumulées valent mieux que les assertions par groupe. La première version levait au premier écart, donc n'en montrait qu'un sur cinq — une liste partielle de ce qui manque se lit comme une liste complète.
- Un auto-contrôle trop serré est un faux positif, pas une trouvaille. Le seuil « au moins deux
paramètres » a signalé
pathToJson, qui en prend légitimement un seul. De même, la première version deDoorDefaultsTestaccusait les deux lecteursdecimalsde réécrire leur défaut : leurDouble.NaNest un sentinelle d'absence, et le vrai replid.decimalsest à la ligne suivante. Assertion corrigée pour chercherd.<champ>dans tout le corps du lecteur. - La porte CLI n'est pas vérifiée, et un
Optqui revendiqueDoor.CLIfait échouer un test exprès. Les trois groupes n'ont aucune option CLI (cliNotele dit par groupe, dans le code) ; leur donner une porte CLI est un ajout de fonctionnalité, pas la mécanique de cette étape.
La moitié « vérification » vaut clairement le catalogue : elle a trouvé cinq écarts réels sur trois
petites classes, et la complétude dérivée est ce qu'aucun test de parité entre portes ne peut faire —
ClimbOptions avait sept champs et les deux portes fil en exposaient six, en parfait accord entre
elles. La moitié « génération » reste non démontrée. Recommandation : faire S9 (élargir le
catalogue, toujours en vérification seule) avant S10, et ne juger les émetteurs que sur le diff
qu'ils produisent contre les lecteurs écrits à la main.
EngineModelJvm.cyclistPowerSpec / coursePhysics / course, et
ElevationProviderJvm.getElevationsAlongBlocking / …Async (plus des fabriques pour
SmoothingOptions et FilterOptions), chacun avec un test Java d'épinglage dans
src/jvmTest/java/. Étendre EngineModelJvmCoverageTest de six fabriques sur un fichier à toutes
les fabriques *Jvm.kt, et ajouter l'assertion de valeur factory() == DataClass() — qui garde
ClimbDetectorJvm, TabularWritersJvm, GpxModelJvm et ElevationProviderJvm, aujourd'hui sans
aucune garde.
Livré : les trois fabriques du moteur, les deux ponts d'élévation et les fabriques
smoothingOptions / filterOptions, chacun épinglé depuis Java (PowerSpecJavaTest,
ElevationProviderJavaTest). EngineModelJvmCoverageTest passe de six fabriques sur un fichier à
quatorze sur quatre façades, et gagne la comparaison de valeurs fabrique() == ClasseDeDonnées().
Non vacuité vérifiée : un défaut dérivé et une fabrique rétrécie échouent tous les deux.
La leçon de conception : @JvmOverloads tronque par la droite, donc l'ordre des paramètres
décide de ce qu'un appelant Java peut omettre. coursePhysics réordonne exprès — la classe de
données met cyclistPowerProvider en dernier, ce qui l'aurait rendu inaccessible sans nommer les
trois fournisseurs auxquels personne ne touche. Découvert en écrivant le test Java, pas en
raisonnant : le premier jet ne compilait pas.
La moitié S6 de ClimbDetectorJvm (sept défauts réécrits) est partie avec, puisque la nouvelle
assertion de valeurs l'exigeait.
EngineJsApi.kt:508/509/517 lit sur un val d = defaultJsOptions(), comme WasiOptions.kt:69 ;
ClimbDetectorJvm.kt:23-31 et EngineModelJvm.kt:75-78,89-95 lisent ClimbOptions.DEFAULT.* et les
défauts des data classes d'étape. Ajouter l'assertion jsNodeTest que {simplifyEnabled: true}
donne SimplifyPathOptions().toleranceM — l'épinglage que WasiOptionsTest.kt:57-61 a déjà et que
JS n'a pas. L'assertion de valeur de S5 garde ensuite la moitié Java pour de bon.
Livré : toEnhanceOptions lit chaque repli sur defaultJsOptions() ou sur l'objet d'options de
l'étape ; plus un seul littéral. Le vrai défaut était pire que « des littéraux » : il y avait deux
sites de défauts qui s'épelaient séparément, donc enhance(path, {}) et enhance(path, null)
pouvaient diverger par construction. EngineJsApiDefaultsTest les épingle ensemble.
Non vacuité vérifiée de la manière la plus directe possible : déplacer SimplifyPathOptions.toleranceM
de 10,0 à 12,0 dans commonMain avec le littéral JS restauré fait échouer le test — c'est
littéralement le scénario « 250 W contre 280 W », rejoué.
La garde sur les quatre DTO d'ElevationJsApi (GetElevationsAlongOptionsDto,
ElevationProviderConfigDto, SmoothingOptionsDto, FilterOptionsDto) et sur le WaypointDto du
moteur, pour qu'une clé mal orthographiée soit une erreur sur toutes les portes JS.
Livré avec S6. Les quatre DTO d'ElevationJsApi et le WaypointDto du moteur — dernier DTO
d'entrée sans garde — refusent une clé inconnue. La validation de getElevationsAlong se fait hors
du GlobalScope.promise, pour qu'une faute de frappe lève au site d'appel plutôt que de devenir une
promesse rejetée : trouvé en écrivant le test, qui échouait sur la première version.
En chemin, un écart que l'audit n'avait pas listé : ElevationJsApi réécrivait aussi 10.0 / 1.0
/ true pour l'échantillonnage, en double avec la signature de getElevationsAlong. Les trois
vivent désormais dans ElevationDefaults (commonMain), lu par le cœur, la façade JS et les ponts
JVM — le même idiome de catalogue partagé que PowerModel et GpxPowerSource.
wireName / fromWire / DEFAULT dans commonMain, à côté de ceux de GpxPowerSource ; les trois
portes y passent ; la préséance est tranchée une fois. Le JsonObj de WASI sait distinguer
absent de fourni, donc rien ne l'oblige à copier la contrainte de non-nullité de JS. Quel que soit le
choix, il est écrit à un seul endroit au lieu de cinq KDoc.
Décision : le préréglage a le dernier mot, sur toutes les portes. Prise par l'auteur du dépôt, sur trois options présentées (préréglage gagnant / explicite gagnant / combinaison refusée).
Ce qui a fait pencher : l'UI de la démo est construite autour de cette règle et l'affiche
(« a wet road overrides the lean angle and braking sliders below », « Overridden by the wet preset
(15.6°, µ = 0.28) ») ; la règle garde les deux limites solidaires, ce qui est l'objet même de R9 ; et
c'est la seule exprimable partout, CyclistDto n'ayant pas de champs nullables.
Livré : RoadCondition.wireName / fromWire / wireNames / DEFAULT plus applyTo, qui est
la règle. Les trois portes y passent. RoadConditionWireTest est en commonTest, donc vérifié sur
JVM, JS Node, JS navigateur et wasmWasi.
Le CLI change : --cyclist-max-angle 42 --road-condition wet donne désormais 15,6°. enhance
avertit sur stderr quand les deux sont passés — un drapeau qui perd en silence contre un
préréglage est contraire aux usages.
Un piège trouvé en cours de route, et c'est la partie qui valait le test. La première version
gardait --road-condition non nullable avec DRY par défaut : le préréglage sec écrasait alors un
--cyclist-max-angle 40 sur une ligne de commande qui ne mentionnait jamais la route. Le cas 05 de
MixinParsingTest a échoué immédiatement. null veut dire « aucun préréglage demandé », ce qui
n'est pas « demander dry » — la même distinction que fait déjà CyclistDto.roadCondition: String?.
S9 — Catalogue v2 : EnhanceOptions / Cyclist / Bike / Wind / puissance, toujours en vérification seule (~1 j)
36 options, plus les 20 lignes CoreOnly de RacingLineOptions. Vert sur develop d'aujourd'hui
pour le groupe enhance ; la valeur est prospective. Ajouter le contrôle d'atteignabilité de la démo
(sur les imports nommés depuis ~/engine-shim, pas sur des occurrences de mots) et un
CliSurfaceTest exigeant que chaque Opt ait une option picocli ou un CliExempt(reason).
Livré, vert sur develop. Quatre groupes de plus (33 options), la complétude devenue récursive,
et deux extracteurs neufs : CliSurfaceTest et DemoReachabilityTest.
Depuis (2026-08-18) :
engine-shim.tsn'existe plus — voir le complément de S10. Le testDemoReachabilityTestlit désormais l'index.d.tsengendré etdemo/src/engine-coverage.md; ce qu'il vérifie n'a pas changé, seulement où il le lit.
Wind a été écarté du catalogue, et c'est un vrai constat plutôt qu'un oubli : WindDto ne
correspond à aucune classe d'options à défauts. windDirection est en degrés et se lit dans
Wind.directionRad en radians, l'absence de l'objet donne WindProviderNone — un autre type — et
non un Wind() par défaut. Le modèle « chemin vers un champ + défaut de la classe » ne s'y applique
pas ; l'y forcer aurait produit une entrée fausse.
Trois catégories, pas deux. roadCondition n'est pas un champ de Cyclist : c'est un préréglage
qui se résout en deux champs. WireOnly est né de là — l'omettre faisait passer CYCLIST_KEYS pour
acceptant une clé morte, ce qui est précisément l'alarme à ne pas banaliser.
Ce que la porte CLI a révélé. Quatre options d'EnhanceOptions n'ont aucun drapeau et rien ne le
consignait : computeMaxSpeeds (câblé en dur à true), simplifyZExaggeration, et les trois
wPrimeBalance*. Deux sont des décisions, deux des manques ; chacune porte désormais sa raison.
Une dérive réelle trouvée par DoorDefaultsTest en chemin : le lecteur WASI de la puissance
écrivait string("type", PowerModel.CONSTANT.id) au lieu de d.model.id, donc un changement du
modèle par défaut de CyclistPowerSpec l'aurait laissé sur CONSTANT.
Et, pour la troisième fois, le piège UP-TO-DATE. CliSurfaceTest et DemoReachabilityTest ont
tous deux atterri verts et aveugles : la liste inputs nommait des fichiers isolés alors que ces
tests parcourent cli/src/main et demo/src entiers. Renommer --cyclist-weight et retirer une
entrée du bloc du shim ne cassaient rien. La règle est corrigée dans codegen/build.gradle.kts :
déclarer le répertoire qu'un extracteur parcourt, pas le fichier auquel on pense — un répertoire
ne peut pas prendre du retard sur un extracteur neuf, une liste de fichiers si.
Décision : ne pas écrire d'émetteur pour l'instant. Ce n'est pas un report faute de temps, c'est la conclusion de la réévaluation que S4 imposait — et elle s'est confirmée à chaque étape depuis.
Ce que S4 exigeait avant tout émetteur : que la moitié « vérification » ait été verte un certain temps. Elle a une poignée de commits. Ce n'est pas « un certain temps ».
Ce que l'expérience a ajouté depuis :
- La vérification a payé, et pas qu'un peu. Elle a trouvé cinq écarts réels en S4, une dérive de
défaut en S9 (
PowerModel.CONSTANT.idau lieu ded.model.id), quatre options CLI sans porte ni trace écrite, et elle a forcé trois catégories de modèle (Opt,CoreOnly,WireOnly) qui disent quelque chose de vrai sur le domaine. L'émetteur n'ajouterait rien à cela. - Le vrai coût n'est pas là où on le croyait. Le travail répétitif n'est pas d'écrire les lecteurs WASI — c'est de se souvenir de toutes les portes. La vérification résout exactement ça ; la génération résoudrait la frappe, qui n'a jamais été le problème.
- Un émetteur déplace le risque, il ne le supprime pas. Le critère d'acceptation prévu était « un diff quasi nul contre les lecteurs écrits à la main ». Un diff nul démontre que l'émetteur sait reproduire ce qui existe déjà — c'est-à-dire qu'il n'apporte rien de vérifiable — et un diff non nul démontre que le modèle est faux. Le test n'a pas de résultat qui plaide pour.
- Le dépôt a déjà un fichier engendré à surveiller (
GeneratedPath.kt), plus la table desurface-coverage.mddepuis S11. Chacun ajoute une manière de committer du périmé.
Quand y revenir. Si WasiOptions.kt devient assez gros pour que l'ajout d'une option y coûte
plus qu'une ligne par lecteur, ou si un cinquième groupe de portes apparaît. Le catalogue est prêt :
il tient déjà les noms fil, les chemins, les défauts et les portes, ce qu'un émetteur demanderait.
Le reste est un StringBuilder.
Ce qui a été fait à la place, et qui était la partie utile de S10 : la table par option de
surface-coverage.md est engendrée depuis le catalogue (S11), avec régénération-et-comparaison. Le
mécanisme « engendrer et vérifier » existe donc dans le dépôt et a un utilisateur — simplement pas
celui que le plan avait prévu.
Suite (2026-08-18) : un second utilisateur, et il ne contredit pas ce qui précède.
:codegen:generateTsFacade engendre l'index.d.ts (plus index.mjs / index.cjs) des deux
paquets npm depuis l'external interface des façades JS. Les quatre arguments ci-dessus visent
l'émission des lecteurs, écrits à la main et qui marchent ; ils ne transposent pas :
- le critère « diff quasi nul » n'a pas de sens ici, parce que l'artefact n'existait pas — le
.d.tsdu compilateur référenceEnhanceOptionsDtosans jamais le déclarer, et le seul corps écrit était le miroir à la main de la démo, hors du paquet publié ; - le vrai coût n'était pas la frappe non plus, mais il n'était pas non plus « se souvenir des
portes » : c'était qu'aucune vérification n'était possible côté type.
DoorKeyParityTestcomparait des noms de clés et jetait tout ce qui suit le:; - engendrer a donc supprimé un test au lieu d'en ajouter un — le contrôle du miroir TypeScript — parce qu'il ne pouvait plus échouer. Un test qui ne peut plus échouer est le vert-aveugle que S4 combattait ;
- le quatrième argument tient toujours : c'est un fichier engendré de plus à surveiller, d'où
TsFacadeTesten régénération-et-comparaison, exactement commeSurfaceLedgerTest.
Ce que ça ne change pas : la note ci-dessous rejetant l'émission de l'external interface côté
Kotlin reste valable, et c'est même elle qui force la direction — Kotlin reste la source, le
TypeScript est dérivé.
docs/ledgers/surface-matrix.tsv (~90-110 lignes, largement transcriptibles depuis les inventaires
par surface de cet audit), avec une colonne JVM/Java ; un SurfaceMatrixTest qui vérifie chaque
cellule de porte contre les extracteurs de S3/S4/S9 au lieu de lui faire confiance ; et une tâche
generateSurfaceLedger qui rend la section « État » de surface-coverage.md depuis le TSV — le
truc « engendrer et comparer » de la fiche w10 pour la table ABI. En parallèle : les contrôles de
réglage des cols dans ClimbsPanel.vue et le badge !report.converged dans useMap.ts /
MapView.vue.
Livré, avec une déviation assumée par rapport au plan. Le plan demandait un
surface-matrix.tsv écrit à la main et un test comparant chaque cellule aux extracteurs. C'est
GeneratePath.FIELDS à nouveau : un fichier dont le seul contenu correct est ce que le code sait
déjà, tenu honnête par un contrôle qui ne doit jamais être en désaccord. Si chaque cellule est
vérifiée contre une dérivation, la dérivation est la source de vérité et le fichier est un cache.
Donc : pas de TSV. La table par option est engendrée depuis OptionCatalog par
./gradlew :codegen:generateSurfaceLedger, entre deux marqueurs de surface-coverage.md, et
SurfaceLedgerTest régénère en mémoire et échoue si ce qui est committé diffère — l'idiome de la
fiche w10. Le reste du document, dont le tableau État et sa colonne Démo, reste écrit à la main :
« atteignable par un humain dans l'UI » n'est pas dérivable, et prétendre le contraire serait le
✅ non vérifié que ce ledger existe pour empêcher.
Les deux manques de la démo sont refermés. Le panneau Cols a six contrôles de réglage pilotant
detectClimbsWithOptions — vérifié dans le navigateur : passer « Min grade » à 20 % fait passer le
Stelvio de un col à zéro et affiche le badge « (tuned) ». La carte affiche un avertissement quand
report.converged est faux.
Ce que je n'ai pas vérifié visuellement : le badge de non-convergence lui-même. Aucun échantillon livré ne fait échouer le solveur (le Stelvio converge en 12 itérations), et je n'ai pas réussi à forcer l'état par l'UI dans un temps raisonnable. Le balisage est typé et la condition triviale, mais c'est une inspection, pas une observation.
codegen/src/main/kotlin/…/surface/OptionCatalog.ktengine/src/jvmTest/kotlin/…/surface/DoorKeyParityTest.kt,DoorArityTest.kt,DoorDefaultsTest.kt,CliSurfaceTest.kt,SurfaceMatrixTest.ktdocs/ledgers/surface-matrix.tsv+ la section « État » desurface-coverage.mdengendrée- Les correctifs de code des familles 1 et 2, étape par étape
./gradlew checkvert à chaque étape (sauf S4, délibérément rouge au commit puis verdie)../tools/wasi/run-all.shcouvre désormaisvcAnalyzeRacingLineJson.- Un aller-retour dans la démo sur un GPX à deux traces et cinq waypoints ressort avec deux traces et cinq waypoints.
--road-condition wet --cyclist-max-angle 42et le{roadCondition:"wet", maxLeanAngleDeg:42}équivalent donnent la même physique de virage sur les trois portes.
Ajouter une option à une classe d'options du cœur fait échouer le build tant que chaque porte ne
l'expose pas ou ne porte pas une raison écrite de ne pas l'exposer — et le tableau d'état de
surface-coverage.md n'est plus édité à la main.
Ce qui a été écarté, et pourquoi.
- Un catalogue
EnhanceOptionCatalogd'exécution danscommonMain, replié par chaque porte. Séduisant — il supprime trois listes de clés et rend un défaut littéral inépelable — mais : il fait payercheckWasmModuleSizesur toutes les cibles ; sa prétention à l'exhaustivité est fausse, car uneList<OptionDescriptor>à laquelle il manque une entrée compile partout, à la différence d'unwhensur un ensemble scellé (PointFieldest la preuve : il est parcouru génériquement par les deux portes fil et ne dérive jamais, et pourtantnanDefaultmanque aux deux, parce que c'est un attribut listé à la main) ; et il impose des changements de signature cassants côté JS pour livrer ses gains CSV/JSON/cols, que le vérificateur livre sans eux. La bonne moitié est conservée : les chemins d'accès dans le catalogue, et le cataloguecommonMainétendu àRoadCondition. - Engendrer l'
external interfaceJS. Vérifié empiriquement plutôt que supposé :generateTypeScriptDefinitions()est déjà actif et le.d.tsémis référenceEnhanceOptionsDtosans jamais le déclarer, tandis que les interfaces qui ont un corps portent une marque__doNotUseOrImplementItqu'aucun littéral d'objet ne satisfait. L'external interfaceest donc forcé pour tout DTO d'entrée, et l'engendrer n'apporte rien que le contrôle de clés n'apporte — tout en faisant pointer les traces d'erreur vers un fichier que personne n'a écrit. - Engendrer les options picocli. Les noms de drapeaux ne sont pas dérivables
(
simplifyEnabled→--simplify,racingLineRoadWidthM→--road-width), les descriptions sont de la prose multiligne avec interpolation${DEFAULT-VALUE},negatable = truesur unBooleannon nul bascule la valeur initiale,--corridoret--gpx-power-sourceont une validation à timing propre, et--cyclist-slewvaut délibérément0.0plutôt que le défaut d'EngineConstants. Un contrôle adossé àCliExempt(reason)est le bon instrument ; un générateur, non. - Une surface de test vitest/npm dans
demo/: une devDependency, une tâche GradlenpmTest, un branchement danschecket une étape CI, pour un contrôle qu'un scan de texte côté JVM fait aussi bien depuis un source set existant. - Un module
:conformanced'emblée. Que lejvmTestd':enginelisedemo/srcetcli/srcinverse le sens des dépendances et c'est un vrai défaut d'odeur — mais c'est du texte, Gradle n'y voit aucun cycle, et S0-S5 sont légitimement chez eux à côté deWasiParityTableTest. À promouvoir si et quand le générateur de ledger atterrit et que les lectures inter-modules se multiplient. - Élargir les 20 autres réglages de
RacingLineOptions.EngineModelJvm.kt:97-104dit qu'ils ont été réglés par la mesure et ne font partie de la surface d'aucune porte, etEngineModelJvmCoverageTest.kt:77épingle l'arité à 3 « pour que l'élargir devienne une décision plutôt qu'un accident ». Le manque, c'est la note de bas de tableau, pas les réglages.
La limite du tout. Une clé qu'un lecteur accepte n'est pas une clé qu'un export utilise.
vcWriteGpxTracks aurait passé chacune des vérifications ci-dessus : bon jeu de clés, bon parseur,
mauvais corps d'export. C'est pour ça que la règle « tout export WASI qui lit un objet d'options doit
avoir un test de comportement sous l'export » est dans CLAUDE.md et pas dans un fichier de test.