substrat.cat
Mètode

El linter com a governança: del consell a la regla que bloqueja

L'espectre que va de l'AGENTS.md a la porta de CI, i com escriure la teva primera regla de linter pròpia per imposar una decisió d'arquitectura.

·9 min de lectura

Hi ha una frase que separa els equips que tenen un harness dels que tenen bones intencions: si no bloqueja, no és una regla.

Aquest és el segon eix del marc de harness engineering: la Restricció, el conjunt de normes que governen què pot produir un agent. I la distinció que ho ordena tot és temporal: hi ha normes que actuen abans que el codi existeixi i normes que actuen després.

L’espectre: de la guia a la imposició

No és una dicotomia, és una gradació de força. Val la pena tenir-la present perquè cada mecanisme té el seu lloc:

Mecanisme Exemple Força Capa
Documentació «Si us plau, feu servir kebab-case» Cap Guia
Convenció Acord d’equip Pressió social Guia
Fitxer d’instruccions AGENTS.md, CLAUDE.md Depèn del model Guia
Avís de linter Regla en mode warn Senyal visible Imposició
Error de linter Regla en mode error Porta que bloqueja Imposició
Correcció automàtica Autofix del linter o formatador Transformació automàtica Imposició
Sistema de tipus Compilador de TypeScript, mypy Rebuig en compilació Imposició

La frontera que importa és la del mig. A dalt, tot és probabilístic: augmentes la probabilitat que l’agent compleixi. A baix, tot és determinista: passa o no passa, i li és igual qui hagi escrit el codi.

El debat públic sobre agents viu gairebé tot a la meitat de dalt. La meitat de baix és la que dona garanties.

Tres maneres de reduir l’espai de decisió

Les restriccions no fan totes la mateixa feina. N’hi ha de tres tipus, de menys a més subtil:

Eliminació. Hi ha una sola opció permesa. «Els fitxers en kebab-case.» No hi ha res a decidir. És la més fàcil d’imposar i la que menys friccions genera.

Canalització. Hi ha vocabulari limitat, però tries dins. «Els serveis acaben en Service, els repositoris en Repository, els adaptadors en Adapter.» L’agent decideix quin és, però no se n’inventa un de nou.

Canonicalització. Decideixis el que decideixis, la correcció automàtica ho porta al mateix lloc. L’ordre dels imports, l’ordre de les funcions dins d’un fitxer, el format. És la més potent perquè no cal que l’agent encerti: n’hi ha prou que produeixi alguna cosa vàlida i l’eina la normalitza.

Si has de començar per una, comença per la canonicalització. És la que dona més consistència per menys baralles.

Escriure la teva primera regla

Un exemple real i complet. La decisió d’arquitectura: les rutes no poden importar repositoris directament; han de passar pels serveis. És una regla clàssica de direcció de dependències, i és exactament el tipus de cosa que un agent es passa per alt cada tres o quatre sessions.

   rutes/       ──▶   serveis/      ──▶   repositoris/
   (HTTP)             (regles)            (persistència)

   rutes/  ──────────────✕──────────────▶  repositoris/
                    prohibit

Escrita a l’AGENTS.md, es compleix la major part de les vegades. Escrita com a regla, es compleix sempre.

En JavaScript o TypeScript, amb ESLint

Una regla pròpia és una funció que rep un context i retorna els nodes que vol visitar:

// eslint-rules/direccio-de-dependencies.js
export default {
  meta: {
    type: 'problem',
    docs: { description: "Les rutes no poden importar repositoris" },
    messages: {
      saltDeCapa: "Les rutes han de passar per un servei, no per '{{ruta}}'.",
    },
  },
  create(context) {
    const fitxer = context.filename
    if (!fitxer.includes('/rutes/')) return {}

    return {
      ImportDeclaration(node) {
        const ruta = node.source.value
        if (/(^|\/)repositoris\//.test(ruta)) {
          context.report({ node, messageId: 'saltDeCapa', data: { ruta } })
        }
      },
    }
  },
}

I al eslint.config.js:

import direccio from './eslint-rules/direccio-de-dependencies.js'

export default [
  {
    files: ['src/**/*.ts'],
    plugins: { casa: { rules: { 'direccio-de-dependencies': direccio } } },
    rules: { 'casa/direccio-de-dependencies': 'error' },
  },
]

Vint-i-cinc línies. A partir d’aquí, la decisió arquitectònica ja no depèn de que ningú se’n recordi.

Si no vols escriure codi de regla

Per a restriccions de dependències, hi ha eines declaratives que ho resolen amb configuració:

Eina Ecosistema Què verifica
dependency-cruiser JavaScript, TypeScript Regles de dependència entre carpetes i mòduls
ArchUnit Java, Kotlin Regles d’arquitectura escrites com a proves
deptrac PHP Capes i transicions permeses
import-linter Python Contractes de capes entre paquets
Ruff Python Estil, imports, i regles pròpies via flake8

Amb dependency-cruiser, la mateixa regla d’abans són vuit línies de configuració:

// .dependency-cruiser.js
forbidden: [{
  name: 'rutes-no-toquen-repositoris',
  severity: 'error',
  from: { path: '^src/rutes' },
  to:   { path: '^src/repositoris' },
}]

La meva recomanació: comença per l’eina declarativa. Passa a la regla pròpia només quan el que vols expressar no hi càpiga.

El tipus com a restricció

La capa més forta de l’espectre no és el linter, és el sistema de tipus, i està infrautilitzada com a eina de govern. Un exemple:

type EurosCents = number & { readonly marca: unique symbol }

function aCents(euros: number): EurosCents {
  return Math.round(euros * 100) as EurosCents
}

function cobrar(import_: EurosCents) { /* … */ }

cobrar(19.99)          // ✕ el compilador ho rebutja
cobrar(aCents(19.99))  // ✓

Cap agent, cap persona i cap presa pot fer passar un import en euros per un en cèntims. No és una regla que algú hagi de recordar: és una impossibilitat. Quan una restricció es pot expressar al sistema de tipus, aquest és el seu lloc.

La porta: on es fa efectiva

Una regla que viu al portàtil d’algú no governa res. La seqüència mínima:

  agent escriu
       │
       ▼
  ganxo de pre-commit ──▶ formatador + autofix   (canonicalitza)
       │
       ▼
  CI ──▶ linter (error) + tipus + proves
       │
       ├── passa ──▶ es pot fusionar
       └── falla ──▶ bloquejat, amb el missatge de la regla

Dos detalls que marquen diferència:

El missatge d’error és la interfície. L’agent el llegeix i corregeix a partir d’ell. «Salt de capa detectat» no serveix; «les rutes han de passar per un servei, no per repositoris/usuaris» sí. Escriu els missatges pensant que qui els llegirà és una màquina que ha d’actuar-hi, perquè cada cop més ho serà.

Entra sempre per warn. Una regla nova en error a un projecte viu bloqueja mig repositori el primer dia. Avís, neteja del que ja hi ha, i llavors error.

Independència de l’agent

El resultat d’una capa d’imposició prou completa és el que dona sentit a tot plegat: la identitat de qui escriu el codi deixa d’importar per al resultat estructural. Tant si la funcionalitat l’ha implementat una desenvolupadora sènior, una becària o un agent, el harness imposa la mateixa signatura estructural.

Dit d’una altra manera: la qualitat estructural passa a ser una propietat del projecte, no de l’autor. Això és el que fa viable treballar amb agents a escala, i és també el motiu pel qual la revisió de codi línia a línia està deixant de tenir sentit com a mecanisme principal de control.

Què costa això

El compromís rigidesa–flexibilitat és inevitable. Com més complet és el harness, més excepcions demana, i cada excepció és manteniment. El principi que OpenAI fa servir per navegar-ho em sembla el correcte: imposar les fronteres centralment, permetre autonomia localment. Regles estrictes per al que travessa tot el projecte —noms, capes, estructura de directoris—; guia lleugera per als patrons de cada domini.

Escriure regles és feina d’enginyeria. Una regla pròpia són un parell d’hores entre escriure-la, provar-la i netejar el que ja incompleix. Es recupera de sobres si l’error es repetia; és malbaratada si era una anècdota.

Les regles poden entrar en conflicte. Dues correccions automàtiques que es desfan mútuament fan que el pipeline no s’estabilitzi mai. És justament el que detecta la prova d’idempotència estructural.

No verifica comportament. Un linter et diu que el fitxer és al lloc correcte i es diu com toca. No et dirà mai que la funció calcula malament l’IVA. Per això les proves segueixen sent la meitat de la història, no un extra.

El que t’has d’endur

  1. El que no bloqueja, no governa. Si és important, ha de ser una porta.
  2. Comença per canonicalitzar —format, ordre, imports—: és el que dona més consistència amb menys friccions.
  3. Primer l’eina declarativa, la regla pròpia només quan calgui.
  4. El que es pugui expressar en tipus, en tipus.
  5. Escriu missatges d’error accionables: els llegirà un agent.
  6. warn primer, error després.

Ara bé: quantes regles necessites? La resposta no és «totes les que se t’acudeixin», i té un criteri concret que veiem a l’article següent.

El següent pas

Tens un procés
que odies fer?

Explica-m'ho i et diré si es pot automatitzar. I si no es pot, també t'ho diré. T'acompanyo en el diagnòstic i ens coneixem. El cafè el poses tu.

hola@substrat.cat