$ dharness

Una puerta de commit para proyectos TypeScript.

ESLint, react-doctor y fallow revisan cada commit antes de que entre, y dharness mutate te dice si tus pruebas notarían que el código se rompe.

~/projects/your-appRuta ocultada
$ git commit -m "feat: add discount helper" 🥊 lefthook v2.1.4  hook: pre-commitdharness: eslint exited with code 1 … the cost note on react-doctor and fallow running at @latest, elided … ── eslint ── src/discount.js  1:8  error  This function is declared at the top of the file with nothing saying what it is for  dharness/require-jsdoc ✖ 1 problem (1 error, 0 warnings) eslint failed, so react-doctor and fallow audit and fallow dupes did not run. There may be more to fix behind it.… the tool’s own --help hint, elided … exit status 1
Salida real de dharness 1.13.1: git commit ejecuta el hook de pre-commit configurado, y ESLint lo bloquea en dharness/require-jsdoc antes de que corran react-doctor o fallow. Se acortó la ruta a su forma relativa y se omitieron dos bloques, ambos señalados.
La puerta

Lo barato primero, y la primera falla corta el resto

Una falla detiene la ejecución y omite las etapas siguientes, así que cada commit paga solo hasta su primer problema. Cada etapa lleva la mediana con la que se midió.

  1. 011008 ms
    ESLint--no-warn-ignored <staged files>

    ¿Este archivo sigue las reglas que recomienda este stack?

    Usa la versión que instaló tu proyecto y el preset que recomienda tu framework, solo sobre lo que preparaste.

    Corre en cuanto tu proyecto lo instala

  2. 022959 ms
    react-doctor--staged --no-dead-code --no-score --no-supply-chain -y

    ¿Este código React es semánticamente correcto?

    Revisa solo los archivos preparados, y su análisis no sale de tu máquina.

  3. 032102 ms
    fallow auditaudit --changed-since HEAD --diff-stdin

    ¿Qué agrega este cambio al grafo del repositorio?

    Juzga el cambio que estás por confirmar y solo bloquea por hallazgos nuevos.

    Corre en cuanto el repositorio tiene un commit contra el cual comparar

  4. 041398 ms

    ¿Cuánta duplicación carga el repositorio?

    El techo de duplicación vive en una configuración que tu proyecto puede leer y sobrescribir, y una falla te entrega los comandos que la leen.

    Corre en cuanto el repositorio tiene un commit contra el cual comparar

Medianas de tres ejecuciones cada una, un proyecto de referencia y la misma lista explícita de archivos preparados para cada etapa. 12 de agosto de 2026.

Primera fallaexit 1
El veredicto

El código de salida es la respuesta, propagado sin cambios. Una etapa que falla nombra las que omitió, así siempre sabes qué no corrió.

Mutación

Averigua si tus pruebas notarían que el código se rompe

Con las pruebas en verde, Stryker rompe tu código un cambio a la vez. Cada ruptura que ninguna prueba nota vuelve con su línea.

  • $ dharness mutate --staged

    Exactamente las líneas que agregó tu cambio preparado

    Juzga el commit que estás por hacer, con el Stryker que ya instaló tu proyecto.

    Banderas (1)
    --exclude-prefix <p> (repeatable)

    Deja fuera del alcance los archivos preparados bajo ese prefijo.

  • $ dharness mutate <path...>

    Archivos con nombre, o líneas con nombre

    Una ruta puede nombrar líneas, como src/thing.ts:12-40, y el veredicto cubre exactamente esas. Los resultados se reutilizan, así la siguiente ejecución es más rápida.

    Banderas (4)
    --dry-run

    Mide cuántas pruebas corre una ejecución acotada, sin mutar nada.

    --concurrency <n> (default 2)

    Cada herramienta calcula su paralelismo sobre la máquina entera, y más workers se midieron más lentos en un alcance pequeño.

    --upgrade
    --fresh

    Mide solo las rutas que nombraste, sin los resultados guardados de ejecuciones anteriores.

Qué decide el código de salida
  • exit 0
    Todos los mutantes son atrapadosEstas pruebas notan que este código se rompe.
  • exit 1
    Un mutante sobreviveSe nombra con su línea y en qué se convirtió.
  • exit 1
    Un mutante que ninguna prueba ejecutóCuenta como no atrapado, nunca como atrapado.
  • exit 1
    Un archivo preparado que ninguna prueba importaSe detiene en related, antes de que arranque Stryker.
~/projects/your-app
$ dharness mutate --stagedoutside Stryker's mutate set: src/price.test.ts… Stryker’s own progress log elided … [Survived] EqualityOperatorsrc/price.ts:11:10-     return price <= 0;+     return price < 0;Tests ran:    isFree is free below zero and not above it… Stryker’s score table elided … 1 file(s), 1 range(s), 5 in-scope mutant(s): 4 killed, 1 survived, 0 no coverage, 0 timeout, 0 errors, 0 ignored 1 mutant(s) survived — a test would not have noticed:   src/price.ts:11 EqualityOperator → price < 0    If equivalent, wrap the statement in `// Stryker disable EqualityOperator: <reason>` … `// Stryker restore EqualityOperator`; next-line does not reach call arguments such as dependency arrays.phases: snapshot 0.1081839s · classify 0.1465392s · discover 0.9919626s · related 0.4707219s · stryker 2.5960918sdharness: 1 mutant(s) survived: a test would not have noticed this code breaking
Salida real de dharness 1.13.1, exit 1. El isFree preparado devuelve price <= 0 y su prueba revisa -1 y 5, así que nada nota que el límite se mueve. Se omitieron dos bloques del registro propio de Stryker, ambos señalados.
Niveles de cobertura

Exige más al código del que depende tu negocio

Declara en coverage-tiers.json qué código es central, una funcionalidad a la vez. El hook del commit mide cada archivo central contra el 100% con tu propio Vitest.

Lo que se exige a cada nivel
  • CORE100%
    Las decisiones de las que depende tu negocio: cada sentencia, rama, función y línea probada.src/features/checkout/
  • IMPORTANT80%
    El código a su alrededor, como la presentación.src/features/checkout/ui/
  • INFRASTRUCTURE0%
    Declaraciones y código de unión, fuera de la cobertura y de las ejecuciones de mutación.src/features/checkout/types.ts
~/projects/your-appRuta ocultada
$ cat .dharness/lefthook.ymlpre-commit:  commands:    dharness:      run: dharness check    coverage:      run: bunx --no-install vitest run --coverage --config .dharness/vitest.coverage.mjs $ git commit -m "feat: add checkout total"╭──────────────────────────────────────╮│ 🥊 lefthook v2.1.4  hook: pre-commit │╰──────────────────────────────────────╯ERROR: Coverage for statements (85.71%) does not meet "src/features/checkout/total.ts" threshold (100%) for src/features/checkout/total.tsERROR: Coverage for branches (75%) does not meet "src/features/checkout/total.ts" threshold (100%) for src/features/checkout/total.tsERROR: Coverage for lines (0%) does not meet "src/features/checkout/ui/format.ts" threshold (80%) for src/features/checkout/ui/format.tsERROR: Coverage for functions (0%) does not meet "src/features/checkout/ui/format.ts" threshold (80%) for src/features/checkout/ui/format.tsERROR: Coverage for statements (0%) does not meet "src/features/checkout/ui/format.ts" threshold (80%) for src/features/checkout/ui/format.ts┃  coverage ❯   RUN  v5.0.3 ~/projects/your-app      Coverage enabled with v8  ✓ src/features/checkout/total.test.ts (2 tests) 3ms  Test Files  1 passed (1)      Tests  2 passed (2)  % Coverage report from v8-------------|---------|----------|---------|---------|-------------------File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -------------|---------|----------|---------|---------|-------------------All files    |      75 |       75 |   66.66 |      80 |                    checkout    |   85.71 |       75 |     100 |     100 |                     total.ts   |   85.71 |       75 |     100 |     100 | 4                  checkout/ui |       0 |      100 |       0 |       0 |                     format.ts  |       0 |      100 |       0 |       0 | 2                 -------------|---------|----------|---------|---------|-------------------… coverage summary elided …exit status 1┃  dharness ❯ … dharness check output elided … summary: (done in 31.21 seconds)✔️ dharness (26.23 seconds)🥊 coverage (4.94 seconds)
Salida real de un commit, dharness 1.13.1 con lefthook 2.1.4 y Vitest 5.0.3, exit 1 y sin commit. El job de cobertura corre junto a la puerta, y total.ts, un archivo central con 75% de ramas, lo detiene. Se reemplazó la ruta y se omitieron dos bloques, ambos señalados.
Configuración

Un comando configura el proyecto, y lo mantiene así

Cada ejecución deriva su plan del repositorio tal como está ahora, así que ejecutarlo meses después reporta la deriva. Una ejecución que falla deja el repositorio como lo encontró.

~/projects/your-appRuta ocultada
$ dharness sync dharness 1.13.1 · sync · ~/projects/your-app   js project       repository root  package manager  npm  test runner      vitest  presets          none matched  owned files      .dharness/ ■ 14 steps · 6 applied · 4 delegated · 4 satisfied · 0 failed   0.99s ── Applied (6) ── ✓ 1/14 install what this project is missing                        0.92s ✓ 2/14 write the files dharness owns                               0.04s ✓ 3/14 keep the thresholds the rules read in .dharness/rules.json  0.00s ✓ 4/14 point .fallowrc.json at the file dharness owns              0.00s ✓ 7/14 point eslint.config.js at the file dharness owns            0.00s ✓ 9/14 give the agent fallow's own tools                           0.00s… the files each step wrote, elided … ── Left to you (4) ── ! 10/14   wire the gate into git… its reason, and steps 11 to 13, elided … … the four steps already in place, elided … ──────────────────────────────────────────────────────────────────────✓ 6 applied · 4 delegated · 4 satisfied · 0 failed   0.99s  exit 0   next  wire the gate into git
Salida real de dharness 1.13.1 sobre un proyecto nuevo con npm y vitest: detectó ambos, aplicó seis de sus catorce pasos y devolvió cuatro. Se reemplazó la ruta y se omitieron tres bloques, todo señalado.
Los catorce pasos, tal como los imprime
  1. 1/14
    install what this project is missing

    ✓ Lo aplica

  2. 2/14
    write the files dharness owns

    ✓ Lo aplica

  3. 3/14
    keep the thresholds the rules read in .dharness/rules.json

    ! Te lo devuelve cuando tu rules.json existe pero no es un objeto JSON

  4. 4/14
    point .fallowrc.json at the file dharness owns

    ! Te lo devuelve cuando tu .fallowrc.json ya tiene configuración propia

  5. 5/14
    resolve the keys this project and dharness both declare

    ! Te lo devuelve cuando tú y dharness declaran la misma clave de fallow

  6. 6/14
    point lefthook.yml at the file dharness owns

    ! Te lo devuelve cuando tu lefthook.yml ya tiene tareas propias

  7. 7/14
    point eslint.config.js at the file dharness owns

    ! Te lo devuelve cuando tu configuración de ESLint es TypeScript, legacy o de una forma que no puede leer

  8. 8/14
    fix the lint config react-doctor silently drops

    ! Te lo devuelve cuando existe un .eslintrc.json legacy

  9. 9/14
    give the agent fallow's own tools

    ✓ Lo aplica

  10. 10/14
    wire the gate into git

    ! Te lo devuelve cuando el proyecto todavía no eligió lefthook o husky

  11. 11/14
    install react-doctor's agent skill

    ! Te lo devuelve cuando siempre, con el comando exacto a ejecutar

  12. 12/14
    decide this project's architecture

    ! Te lo devuelve cuando siempre, porque las zonas son un análisis que escribe tu agente

  13. 13/14
    enforce the coverage tiers in the native test run

    ! Te lo devuelve cuando el repositorio todavía no declara niveles de cobertura, como oferta para adoptarlos

  14. 14/14
    distribute the coverage-tiers skill and its agent references

    ! Te lo devuelve cuando un archivo de instrucciones tiene una región gestionada incompleta o duplicada

Lo que agrega cada preset (4)
  • Next.js
    • eslint-config-next/core-web-vitals
    • eslint-config-next/typescript
    • eslint-plugin-react-doctor · recommended, next

    La capa de TypeScript se suma en cuanto existe un tsconfig.json.

  • Expo
    • eslint-config-expo/flat.js
    • eslint-plugin-react-doctor · recommended, react-native
  • Wails
    • fallow ignorePatterns · wailsjs/**

    Se lee del wailsjsdir que declara tu wails.json.

  • Todo proyecto
    • fallow duplicates · mode semantic
    • minOccurrences 3
    • threshold 3
El paquete complementario

Tamaño, documentación y forma de carpetas, verificados

dharness-eslint-plugin

El tamaño, la documentación y la forma de las carpetas necesitan un número o una política contra la cual verificarse, y este paquete es donde un proyecto la declara.

Un solo registro, todos los archivos que ESLint toca

Un único registro `plugins: { dharness: plugin }` en la configuración que dharness posee es todo el cableado, y pone las seis reglas en cualquier lugar donde ESLint ya se ejecute en ese proyecto. El paquete publica una entrada CommonJS y otra ESM, así que carga en cualquiera de los dos sistemas de módulos.

Ver el código
Las seis reglas que publica
  1. dharness/max-file-lines

    suggestion

    Un archivo que pasa el techo que fijó este proyecto.

    Cada archivo se mantiene lo bastante corto para leerlo de una vez. El techo cuenta código, así que documentar un archivo nunca lo hace pasar el límite; un proyecto que quiere contar comentarios o líneas en blanco los activa.

    Dónde mira

    Cada archivo que ESLint revisa

    Ejecutada sobre un fixture
    ✕ Reportadosrc/price.ts
    .dharness/rules.json
    1{ "maxFileLines": 4 }
    src/price.ts
    1/**2 * Whether an item costs the buyer nothing.3 */4export function isFree(price: number) {5  return price <= 0;6}78export const FREE = 0;9export const LIMIT = 100;
    1. 4:1This file has 5 lines, over the 4 this project allows. Split it.
    Dónde viven los números
    {  "schema": "dharness.rules/v1",  "maxFileLines": 500,  "countComments": false,  "countBlankLines": false,  "roleSuffixes": [".types.ts", ".constants.ts", ".helpers.ts", ".schema.ts"]}

    Los números viven en `.dharness/rules.json`, así que un proyecto puede diferir de otro sin publicar una versión nueva. Un archivo ausente o ilegible cae a los valores por defecto, y todas las demás reglas siguen corriendo.

dharness/max-file-lines

suggestion

Un archivo que pasa el techo que fijó este proyecto.

Cada archivo se mantiene lo bastante corto para leerlo de una vez. El techo cuenta código, así que documentar un archivo nunca lo hace pasar el límite; un proyecto que quiere contar comentarios o líneas en blanco los activa.

Dónde mira

Cada archivo que ESLint revisa

Ejecutada sobre un fixture
✕ Reportadosrc/price.ts
.dharness/rules.json
1{ "maxFileLines": 4 }
src/price.ts
1/**2 * Whether an item costs the buyer nothing.3 */4export function isFree(price: number) {5  return price <= 0;6}78export const FREE = 0;9export const LIMIT = 100;
  1. 4:1This file has 5 lines, over the 4 this project allows. Split it.
Dónde viven los números
{  "schema": "dharness.rules/v1",  "maxFileLines": 500,  "countComments": false,  "countBlankLines": false,  "roleSuffixes": [".types.ts", ".constants.ts", ".helpers.ts", ".schema.ts"]}

Los números viven en `.dharness/rules.json`, así que un proyecto puede diferir de otro sin publicar una versión nueva. Un archivo ausente o ilegible cae a los valores por defecto, y todas las demás reglas siguen corriendo.

Primeros pasos

De la instalación a un commit verificado en cinco pasos

Ejecuta cada paso desde la raíz del repositorio que quieres proteger. Se compila con Go 1.26 o posterior; los archivos de cada versión para Linux, macOS y Windows, en amd64 y arm64, evitan la compilación.

Lo que necesitaAbrir el repositorio
  • Un proyecto JavaScript o TypeScript con su lockfile en git: bun, pnpm, yarn o npm
  • lefthook o husky, para llevar la puerta a git
  • Para la mutación: una configuración JSON de Stryker con vitest o jest
  • Para los niveles de cobertura: Vitest y su proveedor de cobertura, instalados en el proyecto
  • Los proyectos con Yarn Plug’n’Play usan nodeLinker: node-modules antes de mutar
  1. 01

    Instala el binario

    go install github.com/Disble/dharness/cmd/dharness@latest

    Un binario en tu PATH. Si prefieres no compilarlo, las versiones publicadas traen archivos para Linux, macOS y Windows.

  2. 02

    Configura el proyecto

    dharness sync

    Instala lo que falta, escribe .dharness/ y apunta tus configuraciones hacia ahí. Lo que no le corresponde decidir vuelve bajo Left to you, con el motivo.

  3. 03

    Cierra lo que te devuelve

    dharness sync

    Agrega lefthook o husky, pásale a tu agente el paso de arquitectura y vuelve a ejecutarlo. Cada ejecución lista solo lo que sigue abierto y nombra lo siguiente.

  4. 04

    Haz commit como siempre

    git commit

    El hook ejecuta dharness check sobre lo que preparaste. Una falla bloquea el commit y te señala la ayuda de esa misma herramienta.

  5. 05

    Pon a prueba lo que agregaste

    dharness mutate --staged

    Con las pruebas en verde, muta exactamente las líneas que preparaste y nombra cada cambio que tus pruebas dejarían pasar.