Aller au contenu

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.

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 tsconfig autre 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 zod réexporté par shared-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).

Les quatre cas face au checker, au plugin ESLint et à dependency-cruiser : tous signalent un import direct, aucun ne signale du texte dans un commentaire, dependency-cruiser seul nomme le vrai fichier derrière un chemin de tsconfig, aucun ne voit un package réexporté par le noyau partagé

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èque
import { 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.ts
src/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 :

domain/errors/email.error.ts
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 kit
export 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;
}
}

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é.

  • 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 :

De l'écriture d'un fichier à la fusion : check-file après chaque écriture d'un agent, ESLint, Biome et le checker sur les fichiers indexés avant un commit, la vérification de types et les tests unitaires avant un push, chaque script ci sur la pull request, et un seul contrôle obligatoire à la fusion

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 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 :

  • typescript sur npm est en 7.0.2 ;
  • typescript-eslint 8.71.1 déclare la plage de pair typescript >=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.

typescript 6 est chargé par ESLint avec les types et par dependency-cruiser ; @typescript/native, typescript 7, fait la vérification de types et le build

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.

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 architecture
architecture:
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:graph

et 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.

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 pour infrastructure/, lus dans le fichier de config, appliqués à chaque contexte, imposés par verify et 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.)

  • 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/ et main.ts dans 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-reviewer et 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.
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