L'analyse statique et le pipeline
Résumé. L'architecture est tenue par des analyseurs, pas par des commentaires de relecture. Trois d'entre eux partagent un même ensemble d'identifiants de règle, chacun là où il est exact : le checker du kit lit les chemins, le texte et le glossaire ; le plugin ESLint lit l'arbre syntaxique ; dependency-cruiser lit le graphe des imports résolu. knip trouve le code mort, et le compilateur TypeScript en mode strict complète l'ensemble. Un projet généré porte deux versions de TypeScript côte à côte, parce que le compilateur natif (7) n'a pas encore d'API JavaScript et que les outils de lint en ont besoin d'une. Les mêmes portes tournent sur GitHub Actions et sur GitLab CI à partir des mêmes scripts npm. Cette page compare les outils, dit ce qui a été vérifié et ce qui ne l'a pas été, et quand une configuration plus légère suffit.
Pourquoi plusieurs analyseurs
Section intitulée « Pourquoi plusieurs analyseurs »Une même règle comme layer/domain-purity (voir la table des règles de la
référence d'architecture) peut être enfreinte de façons que des outils
différents voient. Chaque cas ci-dessous a été passé aux trois analyseurs sur un petit projet :
- un
import { z } from 'zod'direct dans un objet-valeur : les trois le signalent ; - le même texte dans un commentaire ou une chaîne : une simple recherche textuelle le signalerait ; le lexer du checker saute les commentaires et les chaînes, et l'arbre syntaxique n'y contient aucun import, donc aucun ne le signale, à raison ;
- un import par un chemin de
tsconfigautre que l'alias du projet (#billing-infra/invoice-row, qui mène àinfrastructure/) : le checker et le plugin ESLint lisent le spécificateur tel qu'il est écrit et signalent « le package#billing-infra» ; dependency-cruiser le résout comme le compilateur et nomme le vrai fichier,infrastructure/invoice-row.ts; - un type de
zodréexporté parshared-kernel/index.ts, que le domaine a le droit d'importer : aucun ne le signale. Le checker ne résout pas les réexports, le plugin ESLint voit un import autorisé, et les règles dependency-cruiser générées jugent chaque arête isolément. Ce qu'aucune règle ne contraint se relit à la main (voir Limites).
L'outil le moins coûteux tourne donc le plus souvent, et le plus exact a le dernier mot.
À quoi ressemble un constat, et ce qu'il faut écrire à la place. Le checker répond à l'import direct avec la règle, l'emplacement et la correction :
// À éviter : domain/value-objects/email.vo.ts qui valide avec une bibliothèqueimport { ValueObject } from '@ddd-ts-kit/kernel';import { z } from 'zod';
export class Email extends ValueObject<{ readonly value: string }> { private constructor(props: { readonly value: string }) { super(props); }
static create(value: string): Email { return new Email({ value: z.string().email().parse(value) }); }}$ npx ddd-ts-kit check-file src/contexts/billing/domain/value-objects/email.vo.tssrc/contexts/billing/domain/value-objects/email.vo.ts:2 error layer/domain-purity The domain imports the package "zod". fix: Remove the import: express the need as plain TypeScript or as a port in application/ports, and keep decorators and schemas of "zod" in infrastructure or presentation.
1 error, 0 warnings, 0 suppressions in 1 file.La forme d'une requête HTTP est validée à l'entrée, par le schéma de sa route ; la règle du métier reste en TypeScript simple, avec un refus de son concept :
import { ConceptError, ValueObject } from '@ddd-ts-kit/kernel';
export class EmailError extends ConceptError { static invalid(value: string) { return EmailError.refusal('INVALID_EMAIL', `"${value}" is not an email address.`, { value }); }}
// domain/value-objects/email.vo.ts : le domaine n'importe que le noyau du kitexport class Email extends ValueObject<{ readonly value: string }> { private constructor(props: { readonly value: string }) { super(props); }
static create(value: string): Email { const address = value.trim().toLowerCase();
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(address)) { throw EmailError.invalid(value); }
return new Email({ value: address }); }
get value(): string { return this.props.value; }}Comparaison
Section intitulée « Comparaison »Versions et licences lues sur npm le 2026-10-10.
ddd-ts-kit check |
@ddd-ts-kit/eslint-plugin |
dependency-cruiser | knip | TypeScript (strict) | |
|---|---|---|---|---|---|
| Lit | les chemins, le texte par un petit lexer, CONTEXT.md, la config |
l'arbre syntaxique d'un fichier, les types sur demande | le graphe des modules résolu | les points d'entrée et les exports du projet | le programme entier |
| Fait autorité pour | l'arborescence, les noms, le glossaire, la config, les specs et builders exigés, le barrel généré, l'unicité des noms de consommateurs et de jobs | les règles de code : couches, imports entre contextes, pureté, code propre, forme des agrégats et des gestionnaires, ports, déclarations | les mêmes frontières à travers les barrels et les réexports, les cycles, les orphelins, les dépendances non déclarées | les fichiers, exports, types et dépendances inutilisés | les types |
| Dépendances | aucune | ESLint 9 ou 10, typescript-eslint, @stylistic/eslint-plugin |
dependency-cruiser 18.5.0 (MIT), un analyseur TypeScript | knip 6.41.0 (ISC) | le compilateur |
| Tourne | après chaque écriture de fichier (hook de l'éditeur), sur les fichiers indexés, en CI | sur les fichiers indexés, en CI | en CI | en CI | avant le push, en CI |
| Coût sur le projet d'exemple du README du pipeline (trois contextes, 87 fichiers) | 0,3 s | 4,5 s avec les types | 0,8 s | 0,6 s | 0,4 s avec le compilateur natif |
| Angles morts | pas de compilateur : barrels et réexports non résolus, fonctions fléchées non jugées par certaines règles, heuristiques sur les commentaires | un fichier à la fois : rien sur le projet dans son ensemble | ne dit rien de ce que fait un fichier, seulement de ce qu'il importe | a besoin de points d'entrée corrects ; les fichiers découverts doivent être déclarés comme entrées | ne connaît pas l'architecture |
D'autres outils existent pour des parties de ce travail (plugins ESLint de frontières
d'import, règles de frontières de monorepo, autres détecteurs de code mort). Aucun n'a été évalué
pour cette page. La configuration de dependency-cruiser du kit, .dependency-cruiser.cjs, est
générée à partir de ddd-ts-kit.config.json, et c'est ce qui lui fait suivre la carte des
contextes sans retouche à la main : ddd-ts-kit depcruise-config --write la réécrit
(generate context aussi), et le npm run depcruise généré commence par
depcruise-config --check, qui échoue sur un fichier périmé.
ddd-ts-kit rules liste chaque identifiant de règle avec sa sévérité par défaut. La plupart des
règles de code existent à la fois dans le checker (une approximation rapide sur le texte) et
dans le plugin ESLint (la version exacte). Treize ne sont tenues que par le plugin ESLint, et
la liste le dit : domain/aggregate-encapsulation, domain/value-object-immutability,
domain/no-generic-error, errors/through-factory, errors/constructor-not-public,
errors/factory-name, clean/no-magic-number, application/handler-shape,
application/command-query-separation, presentation/controller-shape,
presentation/controller-delegates, presentation/shared-cache-varies,
presentation/guard-at-the-edge. Une, clean/formatted, n'est tenue par aucun des deux : le
formateur du projet, Biome, met en forme chaque fichier, et la liste indique
« checked by Biome » :
$ npx ddd-ts-kit rules...domain/aggregate-encapsulation error An aggregate or entity has a private or protected constructor, no public mutable property and no setter. (checked by @ddd-ts-kit/eslint-plugin)...clean/formatted error Every TypeScript, JavaScript and JSON file is laid out by the formatter of the project, Biome, at a line width of 120: the format check reports nothing to change. (checked by Biome)Le côté ESLint d'un projet tient en une fonction, qui lit le ddd-ts-kit.config.json placé à
côté d'elle :
// eslint.config.ts (init écrit la même chose dans eslint.config.mjs)import { dddTsKit } from '@ddd-ts-kit/eslint-plugin';
export default dddTsKit({ tsconfigRootDir: import.meta.dirname });Ce que reçoit chaque projet, outil par outil, est sur la page du socle de qualité.
Critères de choix
Section intitulée « Critères de choix »- Le temps de retour. Une règle qui répond en moins d'une seconde peut tourner à chaque écriture. Une règle qui a besoin des types, non. Gardez les deux, dans cet ordre.
- L'exactitude. Quand deux outils sont en désaccord sur une règle de code, l'arbre syntaxique l'emporte : le README du plugin ESLint dit que c'est alors le checker qu'il faut corriger.
- Ce qui doit être jugé à l'échelle du projet. Une spec manquante, un barrel périmé, un nom absent du glossaire ou un cycle dans la carte des contextes ne sont pas des faits d'un seul fichier.
- Le coût d'un faux positif. Une règle heuristique (commentaires de bruit, code commenté) est par défaut un avertissement ; les règles structurelles sont par défaut des erreurs.
Mis bout à bout, un projet généré fait tourner chaque analyseur au moment que son coût permet :
Les hooks du poste de travail sont dans le lefthook.yml qu'écrit init :
pre-commit: parallel: true jobs: - name: eslint glob: '*.{ts,mts,cts,js,mjs,cjs}' run: npx --no -- eslint --no-warn-ignored {staged_files} - name: format glob: '*.{ts,mts,cts,js,mjs,cjs,json,jsonc}' run: npx --no -- biome format --no-errors-on-unmatched {staged_files} - name: architecture glob: 'src/**/*.{ts,mts,cts}' run: npx --no -- ddd-ts-kit check {staged_files} - name: commit size run: npx --no -- ddd-ts-kit check --commit-size --staged
pre-push: jobs: - name: typecheck run: npm run ci:typecheck - name: unit tests run: npm run ci:test:unit
commit-msg: jobs: - name: conventional commit run: npx --no -- commitlint --edit {1}TypeScript 6 et 7 côte à côte
Section intitulée « TypeScript 6 et 7 côte à côte »TypeScript 7 est le compilateur natif. Il compile et vérifie les types beaucoup plus vite, et, tel qu'il est publié, il ne livre aucune API JavaScript. Les outils qui chargent le compilateur comme une bibliothèque ne peuvent pas tourner dessus.
Ce qui a été constaté le 2026-10-10 :
typescriptsur npm est en 7.0.2 ;typescript-eslint8.71.1 déclare la plage de pairtypescript >=4.8.4 <6.1.0, il n'accepte donc pas TypeScript 7 ;- des articles sur la sortie de la 7.0 indiquent qu'une nouvelle API programmatique est attendue dans une version 7.x ultérieure. Si et quand typescript-eslint la prendra en charge n'a pas pu être vérifié dans une déclaration officielle de ce projet.
La réponse du kit, telle que l'énoncent ses README : un projet généré installe les deux.
typescript 6 sert les outils qui lisent les sources par l'API du compilateur (ESLint avec les
types, dependency-cruiser), et @typescript/native, un alias npm de typescript 7, compile et
vérifie les types ; il est appelé par son chemin, parce que les deux packages déclarent un
binaire tsc et que npx tsc en choisirait un.
Dans le package.json d'un projet généré :
{ "scripts": { "typecheck": "node node_modules/@typescript/native/bin/tsc -p tsconfig.json --noEmit", "build": "node node_modules/@typescript/native/bin/tsc -p tsconfig.build.json && ddd-ts-kit build-aliases" }, "devDependencies": { "@typescript/native": "npm:typescript@~7.0.2", "typescript": "~6.0.3", "typescript-eslint": "^8.71.1" }}Avec typescript 7 seul, ESLint avec les types ne démarre pas, et la configuration de
dependency-cruiser se replie sur @swc/core quand il est installé (elle suit alors les imports
de types seuls sans les distinguer) et échoue bruyamment sinon.
Le coût, ce sont deux compilateurs à garder compatibles : une syntaxe acceptée par la 7 et pas
par la 6 passerait la vérification de types puis échouerait au lint. Renovate ou Dependabot
proposeront de faire passer typescript lui-même en 7 ; refusez-le tant que les outils de lint
ne l'acceptent pas.
GitHub Actions et GitLab CI
Section intitulée « GitHub Actions et GitLab CI »La règle du répertoire du pipeline : la logique de CI vit dans des scripts npm, et les deux
fichiers de plateforme ne font qu'orchestrer. Chaque porte tourne donc sur un poste de travail
avec la même commande. La même porte, dans les deux fichiers qu'écrit init :
# .github/workflows/ci.yml : les étapes du job Architecture- name: Architecture rules run: npm run ci:arch:check -- --format github- name: Dependency graph run: npm run ci:arch:graph# .gitlab-ci.yml : le job architecturearchitecture: extends: .node stage: test needs: [install] script: - mkdir -p reports - npm run --silent ci:arch:check -- --format gitlab > reports/gl-code-quality-arch.json - npm run ci:arch:graphet sur un poste de travail, npm run ci:arch, qui lance les deux.
| Élément | GitHub Actions | GitLab CI |
|---|---|---|
| Déclencheurs | les push sur la branche par défaut et les pull requests | les merge requests et la branche par défaut |
| Exécutions redondantes | annulées par des groupes de concurrence | annulées par des jobs interruptibles |
| Portes | les mêmes scripts ci:* |
les mêmes scripts ci:* |
| Constats de lint et d'architecture | des annotations sur la pull request (check --format github) |
des rapports Code Quality (check --format gitlab écrit directement le rapport ; ESLint par son formateur GitLab) |
| Rapports de tests | le reporter Vitest pour Actions, la couverture comme artefact | des rapports JUnit et Cobertura dans la merge request |
| Recherche de secrets | gitleaks | gitleaks |
| Construction d'image sans push | l'action Docker de build | BuildKit sans privilèges (rootless) |
| Vérification du titre | commitlint sur le titre de la pull request | commitlint sur le titre de la merge request |
| Ce qui la rend obligatoire | un job gate comme contrôle obligatoire |
« pipelines must succeed » |
| Mises à jour des dépendances | Renovate ou Dependabot | Renovate |
Les pipelines qu'écrit init n'utilisent que des outils gratuits. Ce qui dépend d'une offre
est à activer (init --ci-extras, ci.extras dans la config) : CodeQL et la revue des
dépendances, gratuits sur les dépôts GitHub publics et payants sur les privés ; le SAST, Secret
Detection (constats dans la merge request sur Ultimate seulement) et Dependency Scanning
(Ultimate) de GitLab. Sur les deux plateformes, les deux contrôles qui bloquent quelle que soit
l'offre sont npm audit et gitleaks. La table de chaque outil, de sa catégorie et des pages des
éditeurs où elle a été lue est dans
pipeline/README.md.
Le choix entre les deux est rarement libre : il suit l'endroit où le code est hébergé. Le but de la conception est que ce choix ne coûte rien dans les portes elles-mêmes.
Ce que le kit prend en charge et a vérifié
Section intitulée « Ce que le kit prend en charge et a vérifié »D'après pipeline/README.md, le
README de la CLI et le
README du plugin ESLint :
- Le checker. Aucune dépendance à l'exécution. Chaque violation a un identifiant de règle,
une sévérité, un emplacement et une phrase
fix;explain <rule-id>en donne la raison avec un mauvais et un bon exemple. Des formats de sortie pour les personnes, JSON, les annotations GitHub et GitLab Code Quality. Un test de génération produit chaque sorte de fichier pour chaque stack et exige zéro violation, si bien que le générateur et le checker ne peuvent pas être en désaccord. - Le plugin ESLint. Configuration flat seulement, ESLint 9 et 10. Une fonction de preset renvoie une configuration complète avec les règles strictes et typées de typescript-eslint ; des règles standard sont activées là où elles couvrent entièrement une convention, et dix règles strictes sont ajustées, chacune pour une raison énoncée. Lancé sur un projet généré, propre puis avec une violation délibérée par règle.
- dependency-cruiser. Configuration dérivée de la config du projet ; lancé en version 18 avec
l'analyseur TypeScript 6 et avec
@swc/core, chaque règle signalant sa violation délibérée. - knip. Fichier, export, type et dépendance inutilisés signalés sur le projet d'exemple.
- Les hooks. lefthook : ESLint, la vérification du format par Biome et le checker sur les
fichiers indexés avant un commit, avec
check --commit-size --stagedà titre indicatif ; vérification de types et tests unitaires avant un push ; commitlint sur le message. - Des seuils de couverture par couche, les plus hauts pour
domain/, les plus bas pourinfrastructure/, lus dans le fichier de config, appliqués à chaque contexte, imposés parverifyet par le pipeline. - Les portes de dérive. La carte des contextes, les parties générées de chaque
CONTEXT.md, les barrels (declarations/barrel-current),.dependency-cruiser.cjs, le document OpenAPI, le document AsyncAPI et le catalogue d'événements sont régénérés et comparés.
Non exécuté, de l'aveu même du README du pipeline : les workflows GitHub ont été analysés
et vérifiés avec actionlint, pas exécutés sur GitHub ; le fichier GitLab a été analysé et ses
étapes résolues par un script, glab ci lint n'a pas été lancé, et il n'a pas été exécuté sur
GitLab ; la protection des branches et les réglages des merge requests ont été écrits d'après
la documentation de l'API.
Les tests de mutation ne sont pas livrés. Le README du pipeline rapporte que Stryker 10,
lancé sur la couche domaine d'un projet généré avec Vitest 5, n'a exécuté aucun test par mutant
et a déclaré tous les mutants survivants, et que son cœur réécrit le tsconfig par une API qui
manque au compilateur natif. Ils seront ajoutés comme job planifié quand l'exécuteur prendra en
charge Vitest 5. (Sur npm, @stryker-mutator/vitest-runner 10.0.0 déclare la dépendance de pair
vitest >=2.0.0, vérifié le 2026-10-10 ; le comportement lui-même n'a pas été relancé pour cette
page.)
Limites, et quand ne pas l'utiliser
Section intitulée « Limites, et quand ne pas l'utiliser »- Le checker est un lecteur de texte. Son README liste ce qu'il manque : réexports et
barrels,
.tsx, fonctions fléchées pour les règles d'ordre de lecture (stepdown) et de taille, imports par alias pour plusieurs règles, un nom de schéma dans du SQL brut, une transaction entre contextes. C'est le filet rapide, pas la preuve. - Le plugin ESLint juge un fichier. Il ne peut pas savoir qu'une spec manque ou que deux consommateurs partagent un nom.
- Rien ne contraint ce qu'importent
shared-kernel/,platform/etmain.tsdans le checker. Relisez-les à la main. - Les analyseurs vérifient la forme, pas la conception. Un agrégat anémique dont les
setters privés ont été renommés en méthodes passe toutes les règles. L'agent
architecture-revieweret la relecture humaine existent pour cela. - Un fichier tout juste généré peut être signalé : un port ou un fake généré avant que quoi que ce soit ne l'utilise est un fichier inutilisé pour knip et un orphelin pour dependency-cruiser, d'après le README du pipeline. Générez-le avec le cas d'usage qui en a besoin.
- Le lint typé est la porte statique la plus lente (des secondes, pas des millisecondes). Sans les types, le preset est plus rapide et perd les règles qui lisent les types.
- Quand une configuration plus légère suffit : un prototype ou un script sans contexte délimité (bounded context) gagne peu aux règles structurelles. Le checker a un mode qui ne lance que les règles de code propre, sur n'importe quel répertoire, sans projet ni config.
- Assouplir une règle est une décision consignée, pas une modification locale : voir
governance/recorded-deviation.
Recommandation selon la situation
Section intitulée « Recommandation selon la situation »| Situation | Choix |
|---|---|
Un projet généré par init |
tout garder tel que généré ; marquer le job de porte unique comme obligatoire |
| Adopter le kit dans une base de code existante | commencer par le checker et le preset ESLint en warn là où l'existant échoue, chaque assouplissement consigné avec son enregistrement de décision ; ajouter dependency-cruiser quand l'arborescence est en place |
| La boucle d'édition d'un agent de code | la vérification par fichier après chaque écriture, la porte complète avant de terminer |
| Un monorepo avec ses propres outils de frontières | les garder pour les frontières entre packages ; les règles du kit s'appliquent toujours à l'intérieur de chaque service |
| TypeScript 7 seul, sans TypeScript 6 | pas possible aujourd'hui avec le lint typé ; garder les deux |
| GitLab en offre gratuite | le pipeline tel que généré : audit et gitleaks bloquent ; les constats des scanners ne sont visibles que dans les journaux des jobs |
| Vouloir des tests de mutation dès maintenant | les lancer à la main sur TypeScript 6 et une version plus ancienne de Vitest dans une branche à part, en sachant que le kit ne les prend pas en charge |
- Ce dépôt :
pipeline/README.md,packages/cli/README.md(sectionscheck, « Limits »),packages/eslint-plugin/README.md,docs/ARCHITECTURE.md(« Who checks what »). - typescript-eslint : https://typescript-eslint.io ; plage de pair lue avec
npm view typescript-eslint peerDependencies. - Annonce de TypeScript 7.0 : https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/
- Configuration flat d'ESLint : https://eslint.org/docs/latest/use/configure/configuration-files
- dependency-cruiser : https://github.com/sverweij/dependency-cruiser
- knip : https://knip.dev
- GitHub Actions : https://docs.github.com/actions ; GitLab CI/CD : https://docs.gitlab.com/ci/
- gitleaks : https://github.com/gitleaks/gitleaks ; lefthook : https://github.com/evilmartians/lefthook ; commitlint : https://commitlint.js.org
- Stryker : https://stryker-mutator.io
- Robert C. Martin, Clean Architecture, Prentice Hall, 2017 (la règle de dépendance que font respecter les analyseurs).
Voir aussi
Section intitulée « Voir aussi »- Les couches et la règle de dépendance
- Code propre
- Exécuteurs de tests
- Discipline des commits
- Organisation du travail en équipe
- Configuration :
rules,limits,deviations,guard