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
- El que no bloqueja, no governa. Si és important, ha de ser una porta.
- Comença per canonicalitzar —format, ordre, imports—: és el que dona més consistència amb menys friccions.
- Primer l’eina declarativa, la regla pròpia només quan calgui.
- El que es pugui expressar en tipus, en tipus.
- Escriu missatges d’error accionables: els llegirà un agent.
warnprimer,errordespré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.