Skrive regler
Regler er TypeScript-funksjoner som sjekker kodebasen din for ADR-samsvar. De ligger i tilhørende .rules.ts-filer ved siden av ADR-markdown-filer og kjøres når du utfører archgate check.
.archgate/adrs/ ARCH-001-command-structure.md # Beslutningen ARCH-001-command-structure.rules.ts # De automatiserte sjekkeneGrunnleggende oppsett
Section titled “Grunnleggende oppsett”Hver regelfil eksporterer et standard-objekt typet med satisfies RuleSet. Hver nøkkel i rules-objektet blir en regel-ID, og hver regel har en description og en asynkron check-funksjon som mottar et kontekstobjekt.
/// <reference path="../rules.d.ts" />
export default { rules: { "my-rule-id": { description: "What this rule checks", async check(ctx) { // Your check logic here }, }, },} satisfies RuleSet;En enkelt regelfil kan definere flere regler:
/// <reference path="../rules.d.ts" />
export default { rules: { "first-rule": { description: "Checks one thing", async check(ctx) { // ... }, }, "second-rule": { description: "Checks another thing", async check(ctx) { // ... }, }, },} satisfies RuleSet;Context API-et
Section titled “Context API-et”ctx-objektet som sendes til hver check-funksjon gir muligheter for fillesing, søk og rapportering. Her er en detaljert referanse med eksempler.
ctx.scopedFiles
Section titled “ctx.scopedFiles”En matrise med filstier som matcher ADR-ens files-glob fra frontmatter. Hvis ADR-en ikke har noe files-felt, inkluderer dette alle prosjektfiler.
for (const file of ctx.scopedFiles) { const content = await ctx.readFile(file); // Check content...}Bruk ctx.scopedFiles når regelen din bare skal gjelde filer ADR-en styrer. For eksempel vil en kommandostrukturregel avgrenset til src/commands/**/*.ts bare motta kommandofiler.
ctx.changedFiles
Section titled “ctx.changedFiles”En matrise med filstier som skiller seg fra grunngrenen, inkludert ikke-committede endringer i arbeidstreet (stagede, ustagede og usporede ikke-ignorerte filer). Auto-detektert som standard, eller fylt fra --staged / --base <ref>. Nyttig for inkrementell sjekking og regler for avhengigheter mellom filer.
// Incremental checking -- only validate changed filesconst filesToCheck = ctx.scopedFiles.filter((f) => ctx.changedFiles.includes(f));
// Cross-file dependency -- if file A changed, file B must also changeif (ctx.changedFiles.includes("config/database.yml")) { if (!ctx.changedFiles.includes("deploy/manifest.yml")) { ctx.report.violation({ message: "config changed but manifest was not bumped", file: "config/database.yml", }); }}ctx.readFile(path)
Section titled “ctx.readFile(path)”Les innholdet i en fil som en streng. Stien er relativ til prosjektroten.
const content = await ctx.readFile("src/cli.ts");ctx.readJSON(path)
Section titled “ctx.readJSON(path)”Les og parse en JSON-fil. Returnerer unknown — cast den til forventet form.
const pkg = (await ctx.readJSON("package.json")) as { dependencies?: Record<string, string>;};ctx.grep(file, pattern)
Section titled “ctx.grep(file, pattern)”Søk i en enkelt fil med et regulært uttrykk. Returnerer en matrise med GrepMatch-objekter, hver med egenskapene file, line, column og content.
const matches = await ctx.grep(file, /console\.error\(/);for (const match of matches) { ctx.report.violation({ message: "Use logError() instead of console.error()", file: match.file, line: match.line, });}ctx.grepFiles(pattern, fileGlob)
Section titled “ctx.grepFiles(pattern, fileGlob)”Søk på tvers av flere filer som matcher et glob-mønster. Returnerer en flat matrise med GrepMatch-objekter fra alle matchende filer. Filer ignorert av .gitignore ekskluderes som standard. Sett respectGitignore: false i ADR-frontmatter for å inkludere dem.
const matches = await ctx.grepFiles(/TODO:/, "src/**/*.ts");for (const match of matches) { ctx.report.warning({ message: "TODO comment found", file: match.file, line: match.line, });}ctx.glob(pattern)
Section titled “ctx.glob(pattern)”Finn filer etter glob-mønster. Returnerer en matrise med filstier relative til prosjektroten. Filer ignorert av .gitignore ekskluderes som standard. Sett respectGitignore: false i ADR-frontmatter for å inkludere dem.
const testFiles = await ctx.glob("tests/**/*.test.ts");ctx.ast(path, language)
Section titled “ctx.ast(path, language)”Parse en kildefil til dens språknative AST. Støttede språk: "typescript", "javascript", "python" og "ruby". TypeScript og JavaScript parses i prosessen til en ESTree Program; Python og Ruby starter systemtolkens parser fra standardbiblioteket som en underprosess. Kaster ved parsefeil eller manglende tolk — den returnerer aldri null.
const program = await ctx.ast("src/cli.ts", "typescript");Se Strukturelle sjekker med ctx.ast() nedenfor for komplette eksempler, og Regel-API-referansen for den returnerte formen per språk.
ctx.report
Section titled “ctx.report”Rapporteringsgrensesnittet med tre alvorlighetsgradsmetoder:
ctx.report.violation(detail)— feil-alvorlighetsgrad (exit-kode 1, blokkerer CI)ctx.report.warning(detail)— advarsel-alvorlighetsgrad (logges, men blokkerer ikke)ctx.report.info(detail)— informasjon (logges for synlighet)
Hver metode aksepterer et objekt med:
| Felt | Type | Påkrevd | Beskrivelse |
|---|---|---|---|
message | string | Ja | Hva bruddet er |
file | string | Nei | Sti til den problematiske filen |
line | number | Nei | Linjenummer for bruddet |
fix | string | Nei | Foreslått løsning (vises til utvikleren) |
ctx.report.violation({ message: "Command file must export a register*Command function", file: "src/commands/check.ts", line: 5, fix: "Add: export function registerCheckCommand(program: Command) { ... }",});ctx.projectRoot
Section titled “ctx.projectRoot”Den absolutte stien til prosjektets rotkatalog. Nyttig når du trenger å konstruere absolutte stier.
Strukturelle sjekker med ctx.ast()
Section titled “Strukturelle sjekker med ctx.ast()”Regulære uttrykk fungerer for overflatemønstre, men kommer til kort for strukturelle spørsmål som “inneholder denne filen bare re-eksporter?” eller “er dette en naken except:-klausul?” — flerlinjesetninger, kommentarer og strenginnhold slår alle beina under linjebasert matching. ctx.ast() parser en fil til et ekte syntakstre slik at regelen din kan sjekke strukturen direkte.
Det returnerte treet er språknativt, ikke enhetlig på tvers av språk:
- TypeScript / JavaScript — en ESTree
Programparset i prosessen av meriyah. TypeScript transpileres først, så typenivå-syntaks (interface, typealiaser,export type { ... } from) fjernes fra treet; en fil som bare inneholder setninger på typenivå parses til en tomProgram-body. - Python — treet fra standardbibliotekets
ast-modul serialisert til JSON. Hver node er et objekt med et_type-felt pluss nodens egne felt og posisjonenelineno/col_offset. - Ruby —
Ripper.sexp-utdata fra standardbiblioteketsRipper: nestede matriser som["program", [["command", ...]]]med[line, column]-posisjonspar.
ctx.ast() kaster ved parsefeil eller manglende tolk — den returnerer aldri null. Kastet er isolert til regelen som feiler og vises som en regelkjøringsfeil (exit-kode 2), slik at et ødelagt miljø dukker opp som en synlig feil i stedet for en falsk godkjenning.
TypeScript: ingen barrel-filer
Section titled “TypeScript: ingen barrel-filer”En barrel-fil er en index.ts der hver setning på toppnivå er en re-eksport. Med ESTree-Program-et blir det spørsmålet en direkte sjekk av setningstyper i stedet for en linjematchende heuristikk:
/// <reference path="../rules.d.ts" />
export default { rules: { "no-barrel-files": { description: "index.ts files must not be pure re-export barrels", async check(ctx) { const indexFiles = ctx.scopedFiles.filter((f) => f.endsWith("/index.ts") );
const checks = indexFiles.map(async (file) => { const program = await ctx.ast(file, "typescript");
const isBarrel = program.body.length > 0 && program.body.every( (node) => node.type === "ExportAllDeclaration" || (node.type === "ExportNamedDeclaration" && node.source !== null) );
if (isBarrel) { ctx.report.violation({ message: `Barrel file detected: ${file} contains only re-exports`, file, fix: "Delete the barrel and import directly from the source modules", }); } });
await Promise.all(checks); }, }, },} satisfies RuleSet;Fordi TypeScript transpileres før parsing, fjernes export type { ... } from-re-eksporter — en barrel som bare inneholder type-re-eksporter parses til en tom body, som body.length > 0-vakten hopper over. Hvis du også trenger å flagge rene type-barrels, kombiner AST-sjekken med en tekstsjekk. Den samme transpileringen forskyver også posisjoner: loc-linjenumre refererer til den transpilerte teksten, ikke den opprinnelige .ts-filen, så en "typescript"-regel må finne konstruksjonen på nytt i den opprinnelige kildekoden (for eksempel med ctx.readFile() og indexOf) før den rapporterer en line — eller utelate line.
JavaScript: ingen require() i ES-moduler
Section titled “JavaScript: ingen require() i ES-moduler”Gå gjennom ESTree-treet etter CallExpression-noder der callee er identifikatoren require. En rekursiv gjennomgang av objektverdier og matriser dekker alle nodetyper uten å måtte liste dem opp:
/// <reference path="../rules.d.ts" />
function findRequireCalls(node: unknown, lines: number[]): void { if (Array.isArray(node)) { for (const item of node) findRequireCalls(item, lines); return; } if (node === null || typeof node !== "object") return; const n = node as EsTreeNode; const callee = n.callee as EsTreeNode | undefined; if ( n.type === "CallExpression" && callee?.type === "Identifier" && callee.name === "require" ) { if (n.loc) lines.push(n.loc.start.line); } for (const value of Object.values(n)) findRequireCalls(value, lines);}
export default { rules: { "no-require-in-esm": { description: ".mjs files must not call CommonJS require()", async check(ctx) { const files = await ctx.glob("src/**/*.mjs");
const checks = files.map(async (file) => { const program = await ctx.ast(file, "javascript"); const lines: number[] = []; findRequireCalls(program, lines); for (const line of lines) { ctx.report.violation({ message: "CommonJS require() call in an ES module", file, line, fix: "Use a static import or await import() instead", }); } });
await Promise.all(checks); }, }, },} satisfies RuleSet;loc.start.line-rapporteringen her er gyldig bare fordi "javascript" parser den opprinnelige kildekoden utranspilert — en "typescript"-regel må i stedet finne linjen i den opprinnelige kildekoden, siden dens loc refererer til det transpilerte resultatet.
Python: ingen nakne except-klausuler
Section titled “Python: ingen nakne except-klausuler”I Pythons ast-modul er en except:-klausul en ExceptHandler-node der type-feltet holder uttrykket for unntaket som fanges — en naken except: har "type": null. Gå gjennom JSON-treet etter den formen:
/// <reference path="../rules.d.ts" />
function findBareExcepts(node: unknown, lines: number[]): void { if (Array.isArray(node)) { for (const item of node) findBareExcepts(item, lines); return; } if (node === null || typeof node !== "object") return; const n = node as PythonAstNode; if (n._type === "ExceptHandler" && n.type === null) { if (n.lineno !== undefined) lines.push(n.lineno); } for (const value of Object.values(n)) findBareExcepts(value, lines);}
export default { rules: { "no-bare-except": { description: "Python code must not use bare except: clauses", async check(ctx) { const files = await ctx.glob("**/*.py");
const checks = files.map(async (file) => { const tree = await ctx.ast(file, "python"); const lines: number[] = []; findBareExcepts(tree, lines); for (const line of lines) { ctx.report.violation({ message: "Bare except: catches every exception, including SystemExit", file, line, fix: "Catch a specific exception class, e.g. except ValueError:", }); } });
await Promise.all(checks); }, }, },} satisfies RuleSet;Ruby: ingen puts-kall
Section titled “Ruby: ingen puts-kall”Ripper.sexp representerer puts "hello" som ["command", ["@ident", "puts", [1, 0]], [...args]] — [line, column]-paret ligger inne i @ident-tokenet. Gå gjennom de nestede matrisene etter den formen:
/// <reference path="../rules.d.ts" />
function findPutsCalls(node: unknown, lines: number[]): void { if (!Array.isArray(node)) return; const [kind, first] = node; if ( kind === "command" && Array.isArray(first) && first[0] === "@ident" && first[1] === "puts" ) { const [line] = first[2] as [number, number]; lines.push(line); } for (const child of node) findPutsCalls(child, lines);}
export default { rules: { "no-puts": { description: "Ruby code must use the application logger, not puts", async check(ctx) { const files = await ctx.glob("app/**/*.rb");
const checks = files.map(async (file) => { const sexp = await ctx.ast(file, "ruby"); const lines: number[] = []; findPutsCalls(sexp, lines); for (const line of lines) { ctx.report.violation({ message: "puts writes to stdout directly", file, line, fix: "Replace puts with logger.info", }); } });
await Promise.all(checks); }, }, },} satisfies RuleSet;Ripper bruker en annen nodeform for varianten med parenteser — puts("hello") vises under et method_add_arg / fcall-par i stedet for command — så en produksjonsregel ville matchet fcall-tokenet på samme måte.
Alvorlighetsgrader
Section titled “Alvorlighetsgrader”Hver regel kan sette en standard alvorlighetsgrad i konfigurasjonen sin. Alvorlighetsgraden bestemmer hvordan brudd behandles:
| Alvorlighetsgrad | Exit-kode | Oppførsel |
|---|---|---|
error | 1 | Blokkerer CI, må fikses |
warning | 0 | Logges, men blokkerer ikke |
info | 0 | Informasjon, logges for synlighet |
Sett alvorlighetsgraden i regeldefinisjonen:
export default { rules: { "my-rule": { description: "...", severity: "warning", async check(ctx) { // Violations from this rule are warnings, not errors ctx.report.violation({ message: "..." }); }, }, },} satisfies RuleSet;Hvis severity utelates, er standardverdien error.
Du kan også rapportere med ulike alvorlighetsgrader innenfor samme regel ved å bruke ctx.report.violation(), ctx.report.warning() og ctx.report.info() direkte.
Tidsgrense for regler
Section titled “Tidsgrense for regler”Hver regel har en kjøringstidsgrense på 30 sekunder. Hvis en regel overskrider denne grensen, behandles den som en feil. Dette forhindrer at uendelige sjekker blokkerer pipelinen.
Hold reglene raske ved å:
- Bruke
ctx.grepFiles()i stedet for å lese hver fil manuelt - Bruke
Promise.all()for å sjekke filer parallelt - Avgrense regler med
files-feltet i frontmatter for å begrense antall filer som behandles
fix-feltet
Section titled “fix-feltet”fix-feltet er en valgfri streng som vises til utvikleren sammen med bruddmeldingen. Den beskriver hvilken handling som må utføres for å løse problemet. Fikser brukes ikke automatisk — de er veiledning.
ctx.report.violation({ message: `Unapproved dependency: "chalk"`, file: "package.json", fix: "Use styleText() from node:util instead of chalk",});Når den vises, kommer fiksen under bruddmeldingen:
ARCH-006/no-unapproved-deps package.json Unapproved dependency: "chalk" Fix: Use styleText() from node:util instead of chalkTips for å skrive regler
Section titled “Tips for å skrive regler”-
Bruk
Promise.all()for parallelle filsjekker. Når du sjekker flere filer uavhengig, prosesser dem parallelt i stedet for sekvensielt.// Good: parallelconst checks = files.map(async (file) => {const content = await ctx.readFile(file);// ...});await Promise.all(checks);// Avoid: sequentialfor (const file of files) {const content = await ctx.readFile(file);// ...} -
Bruk
ctx.changedFilesfor inkrementell sjekking.ctx.changedFilesfylles automatisk med grenforskjellen pluss ikke-committede endringer i arbeidstreet (eller stagede filer med--staged). Filtrerctx.scopedFilesmot den for å bare sjekke det som er endret, eller bruk den direkte for regler om avhengigheter mellom filer. -
Hold reglene fokusert på ett anliggende. En regel som sjekker både navnekonvensjoner og importmønstre bør deles i to regler med separate ID-er.
-
Bruk
ctx.grepFiles()fremfor manuell iterering. Når du søker etter et mønster på tvers av mange filer, erctx.grepFiles()mer effektivt enn å lese hver fil og kjøre et regulært uttrykk. -
Gi handlingsbare
fix-meldinger. En fiks som “Ikke gjør dette” er ikke nyttig. Fortell utvikleren nøyaktig hva som skal gjøres i stedet. -
Filtrer bort ikke-relevante filer tidlig. Hvis regelen din bare gjelder for bestemte filer innenfor omfanget, filtrer
ctx.scopedFilesfør prosessering:const commandFiles = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts")); -
Håndter manglende filer elegant. Hvis regelen din leser en spesifikk fil som
package.json, pakk lesingen i en try/catch og returner tidlig hvis filen ikke eksisterer.
Opt-out-direktiver
Section titled “Opt-out-direktiver”Det finnes to måter å håndtere unntak i Archgate: undertrykkelse på motornivå (fungerer med alle regler automatisk) og egendefinerte direktiver på regelnivå (implementert av regelforfatteren for domenespesifikke opt-outs).
Undertrykkelse på motornivå
Section titled “Undertrykkelse på motornivå”Archgate støtter inline archgate-ignore-kommentarer som undertrykker brudd uten å endre selve regelen. Motoren analyserer disse kommentarene og filtrerer matchende brudd før rapportering.
Neste-linje-undertrykkelse: undertrykker bruddet på den umiddelbart følgende linjen:
// archgate-ignore ARCH-006/no-unapproved-deps legacy-avhengighet, migrering planlagt for Q3import chalk from "chalk";Filnivå-undertrykkelse: undertrykker alle matchende brudd hvor som helst i filen:
// archgate-ignore-file ARCH-005/test-mirrors-src generert fil, ingen manuell testFlere regler: stable kommentarer for å undertrykke mer enn én regel på samme linje:
// archgate-ignore ARCH-006/no-unapproved-deps legacy-avhengighet// archgate-ignore ARCH-003/use-style-text tredjepartsbibliotek håndterer fargerimport chalk from "chalk";Sammenhengende undertrykkelseskommentarer sikter alle mot den første ikke-undertrykkelseslinjen etter blokken.
Formatet er ADR-ID/rule-id etterfulgt av en begrunnelse. Begrunnelsen er påkrevd. En undertrykkelse uten begrunnelse ignoreres og gir en advarsel:
[suppression] Suppression for ARCH-006/no-unapproved-deps is missing a reason src/foo.ts:1Både // og # kommentarstiler støttes, slik at undertrykkelser fungerer i TypeScript, JavaScript, YAML, Python, shell-skript og andre filtyper reglene dine kan skanne.
Egendefinerte direktiver på regelnivå
Section titled “Egendefinerte direktiver på regelnivå”For domenespesifikke opt-outs kan regelforfattere implementere sine egne kommentarbaserte direktiver inne i check-funksjonen. Dette mønsteret gir regelen full kontroll over direktivsyntaksen, plassering og validering.
// I din .rules.ts-fil:async check(ctx) { const files = await ctx.glob("src/components/**/*Connected.tsx");
for (const file of files) { const content = await ctx.readFile(file);
// Støtt opt-out-direktiv øverst i filen if (/^\/\/\s*@no-presentational:/u.test(content.trimStart())) continue;
// ... regellogikk som kan rapportere et brudd ... ctx.report.violation({ message: "Manglende presentasjonskomponent", file, fix: 'Legg til "// @no-presentational: <begrunnelse>" øverst i filen for å gjøre opt-out', }); }}Utvikleren gjør opt-out ved å legge til direktivet i filen sin:
// @no-presentational: denne komponenten bare omdirigerer, ingen UI å rendreimport { useNavigate } from "react-router";Når bruke hva
Section titled “Når bruke hva”| Tilnærming | Best for | Hvem kontrollerer |
|---|---|---|
archgate-ignore | Ad-hoc-unntak for alle regler | Utvikleren som bruker regelen |
| Egendefinert direktiv | Domenespesifikke opt-outs med strukturerte begrunnelser | Regelforfatteren |
Bruk archgate-ignore når en utvikler trenger å undertrykke et engangsbrudd. Bruk egendefinerte direktiver når opt-outen er et førsteklasses konsept i regelens domene, for eksempel å markere en komponent som med vilje uparet, eller en fil som automatisk generert.
Neste steg
Section titled “Neste steg”- Vanlige regelmønstre — Kopier-og-lim-inn-mønstre organisert etter kategori: avhengighetshåndtering, importrestriksjoner, filstruktur, kodekvalitet, databaseskjema og arkitekturgrenser.
- Regel-API-referanse — Fullstendig referanse for alle regel-API-typer og -funksjoner.
- CI-integrasjon — Koble
archgate checktil pipelinen din for å håndheve regler på hver PR.