Rule API
Archgate rules are TypeScript files that export a plain object typed with satisfies RuleSet. Each rule receives a RuleContext with utilities for searching files, reading content, and reporting violations.
RuleSet
Section titled “RuleSet”/// <reference path="../rules.d.ts" />
export default { rules: { "my-rule-id": { description: "Human-readable description of what this rule checks", severity: "error", // optional, defaults to "error" async check(ctx) { // Rule logic here }, }, },} satisfies RuleSet;A rules file default-exports a plain object with a rules record keyed by rule ID. Keys become the rule IDs that appear in check output and violation reports. The satisfies RuleSet annotation provides type checking without wrapping in a function call.
type RuleSet = { rules: Record<string, RuleConfig> };RuleConfig
Section titled “RuleConfig”Each rule in the record must conform to the RuleConfig interface.
interface RuleConfig { description: string; severity?: Severity; check: (ctx: RuleContext) => Promise<void>;}| Field | Type | Required | Description |
|---|---|---|---|
description | string | Yes | Human-readable description shown in check output |
severity | Severity | No | Default severity for violations. Defaults to "error" |
check | (ctx: RuleContext) => Promise<void> | Yes | Async function containing the rule logic |
RuleContext
Section titled “RuleContext”The check function receives a RuleContext object with the project state and utility methods.
interface RuleContext { projectRoot: string; scopedFiles: string[]; changedFiles: string[]; glob(pattern: string): Promise<string[]>; grep(file: string, pattern: RegExp): Promise<GrepMatch[]>; grepFiles(pattern: RegExp, fileGlob: string): Promise<GrepMatch[]>; readFile(path: string): Promise<string>; fileAtBase(path: string): Promise<string | null>; readJSON(path: string): Promise<unknown>; ast(path: string, language: AstLanguage, opts?: AstOptions): Promise<AstNode>; report: RuleReport;}Properties
Section titled “Properties”projectRoot
Section titled “projectRoot”projectRoot: string;Absolute path to the project root directory (where .archgate/ lives).
scopedFiles
Section titled “scopedFiles”scopedFiles: string[];Files matching the ADR’s files glob patterns from its frontmatter. If the ADR has no files field, this contains all project files. Use this as the primary file list for your rule checks.
changedFiles
Section titled “changedFiles”changedFiles: string[];Files that have been modified according to git. By default, this is auto-populated with the branch diff against the detected base branch (e.g., origin/main) plus any uncommitted working-tree changes (staged, unstaged, and untracked non-ignored files). When --staged is used, this contains only staged files. When --base <ref> is used, this contains all files changed since that ref plus uncommitted working-tree changes. Empty when base detection fails or no changes are found. Use this to build cross-file dependency rules (e.g., “if file A changed, file B must also change”).
report
Section titled “report”report: RuleReport;The reporting interface for recording violations, warnings, and informational messages. See RuleReport below.
Methods
Section titled “Methods”glob(pattern: string): Promise<string[]>;Find files matching a glob pattern relative to the project root. Returns an array of file paths. Files ignored by .gitignore are excluded by default. Set respectGitignore: false in the ADR frontmatter to include them.
const testFiles = await ctx.glob("tests/**/*.test.ts");grep(file: string, pattern: RegExp): Promise<GrepMatch[]>;Search a single file for lines matching a regular expression. Returns an array of GrepMatch objects with file path, line number, column, and matched content.
const matches = await ctx.grep(file, /console\.error\(/);grepFiles
Section titled “grepFiles”grepFiles(pattern: RegExp, fileGlob: string): Promise<GrepMatch[]>;Search multiple files matching a glob pattern for lines matching a regular expression. Combines glob and grep into a single call. Files ignored by .gitignore are excluded by default. Set respectGitignore: false in the ADR frontmatter to include them.
const matches = await ctx.grepFiles(/TODO:/i, "src/**/*.ts");readFile
Section titled “readFile”readFile(path: string): Promise<string>;Read the contents of a file as a string. The path is relative to the project root.
const content = await ctx.readFile("src/config.ts");fileAtBase
Section titled “fileAtBase”fileAtBase(path: string): Promise<string | null>;Read a file’s source at the comparison base revision — the merge base of the --base ref and HEAD, the same commit changedFiles is computed against. Use it to compare the working tree against the point the change set diverged from.
Returns null in the two “nothing to compare against” cases, so a single null check covers both:
- No base is resolved — the check ran without
--base(or the histories are unrelated). - The file did not exist at the base — an added file.
const before = await ctx.fileAtBase("data/schema.py");if (before === null) { // No base version to compare against -- skip. return;}const after = await ctx.readFile("data/schema.py");For a structural comparison (ignoring comments and formatting) prefer ast(path, language, { rev: "base" }) below.
readJSON
Section titled “readJSON”readJSON(path: string): Promise<unknown>;Read and parse a JSON file. The path is relative to the project root. Returns the parsed value as unknown — cast to the expected type in your rule.
const pkg = (await ctx.readJSON("package.json")) as { dependencies?: Record<string, string>;};ast( path: string, language: "typescript" | "javascript" | "python" | "ruby", opts?: AstOptions): Promise<AstNode>;
interface AstOptions { rev?: "base"; comments?: boolean;}Parse a source file into its language-native AST. The path is relative to the project root and passes through the same sandbox as readFile. TypeScript and JavaScript are parsed in-process; Python and Ruby are parsed by invoking the system interpreter’s own standard-library AST facility as a subprocess. The returned tree shape differs per language — see AstNode.
const program = await ctx.ast("src/cli.ts", "typescript");for (const node of program.body) { console.log(node.type);}Parsing the base revision ({ rev: "base" })
Section titled “Parsing the base revision ({ rev: "base" })”With { rev: "base" }, ast() parses the file at the comparison base revision instead of the working tree — everything else (return shape, throw contract) is identical. Parse both revisions to ask “did the executable structure change?” Comments are absent from the ESTree and Python ast shapes, but node positions are not: loc / lineno fields shift when a comment or blank line moves the lines below it. Compare a location-free projection — strip loc / range (ESTree) and lineno / col_offset (Python) before comparing — so a comment-only edit reads as unchanged. (Python docstrings are string nodes in the tree, so an edited docstring is a real change; strip those too if doc edits should be neutral.)
// Flag a change only when the executable structure actually changed.// `structurallyEqual` compares the trees with position metadata (loc/range,// lineno/col_offset) stripped -- see the writing-rules guide for a concrete// implementation.for (const file of ctx.changedFiles.filter((f) => f.endsWith(".py"))) { // Skip files with no base counterpart -- ast({ rev: "base" }) would throw. if ((await ctx.fileAtBase(file)) === null) continue; const before = await ctx.ast(file, "python", { rev: "base" }); const after = await ctx.ast(file, "python"); if (!structurallyEqual(before, after)) { ctx.report.violation({ message: `${file} changed behavior`, file }); }}Reach for fileAtBase() first when you need to detect the no-base or added-file cases as ordinary control flow — ast({ rev: "base" }) throws for them (see below).
Collecting comments ({ comments: true })
Section titled “Collecting comments ({ comments: true })”With { comments: true }, the returned tree carries a comments array — structured comment data for comment-governance rules, in place of line-by-line regex. Supported for typescript, javascript, and python; requesting it for ruby throws.
interface CommentToken { type: "line" | "block"; value: string; // delimiters (`//`, `/* */`, `#`) removed loc: { start: { line: number; column: number }; end: { line: number; column: number }; };}const tree = await ctx.ast("src/api.ts", "typescript", { comments: true });for (const comment of tree.comments ?? []) { const lines = comment.value.split("\n").length; if (lines > 10) { ctx.report.warning({ message: "Comment block is too long -- link to an ADR instead", file: "src/api.ts", line: comment.loc.start.line, }); }}Comment positions are accurate against the original source, even for TypeScript. This is a deliberate advantage over the tree’s own loc, which is transpiled-relative for TypeScript (see AstNode): comments are scanned from the pre-transpile source, so their loc never drifts. Python comments are always type: "line" (#) — Python has no block comments, and """ docstrings are string expressions in the tree, not comments. The TypeScript/JavaScript scanner is string- and template-literal-aware, but does not track regular-expression literals, so a comment delimiter inside a regex literal is a known blind spot.
Failure behavior
Section titled “Failure behavior”ast() throws on failure — it never returns null:
- Parse failure: the file does not parse as the requested language. The error message includes the parser’s diagnostic.
- Missing interpreter (
python/rubyonly): no suitable interpreter was found onPATH. - Implausible input: the file’s extension does not match the requested language (e.g.
ctx.ast("config.json", "python")throws before any interpreter is invoked). - No base revision (
{ rev: "base" }only): the check ran without--base, so there is no base to parse. UsefileAtBase()to detect this asnullinstead. - File absent at base (
{ rev: "base" }only): the path was added since the base and did not exist there.
A thrown error is isolated to the failing rule: other rules and ADRs in the same check run continue normally, and the failure surfaces as a rule execution error with exit code 2 (distinct from exit code 1 for violations). The throw cases are distinguishable by message text, so check output tells “this environment cannot run this rule” apart from “this file has a syntax error” or “there is no base revision”.
RuleReport
Section titled “RuleReport”The reporting interface for recording check results. Each method accepts a detail object describing the issue.
interface RuleReport { violation(detail: ReportDetail): void; warning(detail: ReportDetail): void; info(detail: ReportDetail): void;}violation
Section titled “violation”report.violation(detail: ReportDetail): void;Report a rule violation. Violations cause the check to fail with exit code 1. Use for hard constraints that must not be merged.
warning
Section titled “warning”report.warning(detail: ReportDetail): void;Report a warning. Warnings appear in check output but do not cause the check to fail. Use for non-blocking guidance.
report.info(detail: ReportDetail): void;Report an informational message. Does not affect the check exit code. Use for suggestions or notes.
ReportDetail
Section titled “ReportDetail”The detail object passed to violation, warning, and info.
interface ReportDetail { message: string; file?: string; line?: number; endLine?: number; endColumn?: number; fix?: string;}| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Human-readable description of the issue |
file | string | No | File path where the issue was found |
line | number | No | Start line number (1-based) |
endLine | number | No | End line number (1-based), for precise editor range highlighting |
endColumn | number | No | End column number (0-based), for precise editor range highlighting |
fix | string | No | Suggested fix or remediation action |
When endLine and endColumn are provided, editors (VS Code, Cursor) can highlight the exact expression that violates the rule, rather than the entire line. If omitted, the full line at line is highlighted.
GrepMatch
Section titled “GrepMatch”Returned by ctx.grep() and ctx.grepFiles().
interface GrepMatch { file: string; line: number; column: number; content: string;}| Field | Type | Description |
|---|---|---|
file | string | Project-relative path to the matched file |
line | number | Line number of the match (1-based) |
column | number | Column number of the match (1-based) |
content | string | Full content of the matched line |
AstNode
Section titled “AstNode”Returned by ctx.ast(). The shape is language-native and deliberately not unified across languages — each language returns its own standard AST vocabulary, so a rule inspecting Python code works against a different grammar than one inspecting TypeScript.
type AstLanguage = "typescript" | "javascript" | "python" | "ruby";type AstNode = Record<string, unknown> | unknown[];When parsed with { comments: true }, the root node also carries a comments: CommentToken[] array (typescript, javascript, and python only). It is absent otherwise.
| Language | Backing parser | Returned shape |
|---|---|---|
typescript | meriyah, in-process, after transpiling TypeScript away with Bun.Transpiler | ESTree Program with loc position info. Type-only syntax (interface, type aliases, export type { ... } from) is erased before parsing — a file containing only type-level statements parses to an empty Program body. loc positions refer to the transpiled output, not the original .ts file — dropped type-only statements, comments, and blank lines make line numbers drift, so re-locate the construct in the original source (e.g. ctx.readFile() plus indexOf) before reporting a line, or omit line entirely; loc is source-accurate only for javascript |
javascript | meriyah, in-process | ESTree Program with loc position info |
python | Python’s standard-library ast module, via the system interpreter | JSON-serialized ast nodes: { "_type": "Module", "body": [...] } — each node carries _type, the node’s own fields, and lineno / col_offset positions |
ruby | Ruby’s standard-library Ripper, via the system interpreter | Ripper.sexp nested arrays: ["program", [["command", ...]]] with [line, column] position pairs embedded in token entries |
See Structural checks with ctx.ast() for a complete example rule per language.
Severity
Section titled “Severity”type Severity = "error" | "warning" | "info";| Value | Exit code impact | Description |
|---|---|---|
"error" | Causes exit 1 | Hard constraint, blocks merges |
"warning" | No impact | Non-blocking guidance |
"info" | No impact | Informational, suggestions only |
ViolationDetail
Section titled “ViolationDetail”The internal representation of a reported issue, used in check output and JSON results.
interface ViolationDetail { ruleId: string; adrId: string; message: string; file?: string; line?: number; endLine?: number; endColumn?: number; fix?: string; severity: Severity;}| Field | Type | Description |
|---|---|---|
ruleId | string | Rule ID from the rules object key |
adrId | string | ADR ID from the frontmatter |
message | string | Human-readable description |
file | string? | File path where the issue was found |
line | number? | Start line number (1-based) |
endLine | number? | End line (1-based), for precise editor highlighting |
endColumn | number? | End column (0-based), for precise editor highlighting |
fix | string? | Suggested fix |
severity | Severity | Effective severity of this violation |
Inline suppression
Section titled “Inline suppression”Violations can be suppressed in source code using archgate-ignore comments. The engine handles this automatically. Rules do not need any special logic.
// archgate-ignore ARCH-006/no-unapproved-deps legacy dep, migration plannedimport chalk from "chalk";A reason is required. File-level suppression uses archgate-ignore-file. Stack multiple comments to suppress more than one rule on the same line. See Opt-out directives for full details and custom directive patterns.