---
title: Rule API
description: TypeScript API reference for writing Archgate rules. Learn RuleSet with satisfies, RuleContext, violation reporting, file matching, and AST-based compliance checks.
---
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
```typescript
///
export default {
rules: {
"my-rule-id": {
description: "Human-readable description of what this rule checks",
async check(ctx) {
// Rule logic here -- the report method decides the severity
ctx.report.violation({ message: "..." });
},
},
},
} 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.
```typescript
type RuleSet = { rules: Record };
```
---
## RuleConfig
Each rule in the record must conform to the `RuleConfig` interface.
```typescript
interface RuleConfig {
description: string;
severity?: Severity;
check: (ctx: RuleContext) => Promise;
}
```
| Field | Type | Required | Description |
| ------------- | ------------------------------------- | -------- | ------------------------------------------------ |
| `description` | `string` | Yes | Human-readable description shown in check output |
| `severity` | `Severity` | No | Accepted and validated, but inert -- see below |
| `check` | `(ctx: RuleContext) => Promise` | Yes | Async function containing the rule logic |
:::caution
`severity` does **not** set the severity of the rule's findings. Each finding takes its severity from the `ctx.report.*` method that produced it: `violation()` is always `error`, `warning()` is `warning`, `info()` is `info` -- even inside a rule declared `severity: "warning"`. To report non-blocking findings, call `ctx.report.warning()`.
:::
---
## RuleContext
The `check` function receives a `RuleContext` object with the project state and utility methods.
```typescript
interface RuleContext {
projectRoot: string;
scopedFiles: string[];
changedFiles: string[];
glob(pattern: string): Promise;
grep(file: string, pattern: RegExp): Promise;
grepFiles(pattern: RegExp, fileGlob: string): Promise;
readFile(path: string): Promise;
fileAtBase(path: string): Promise;
readJSON(path: "package.json"): Promise;
readJSON(path: string): Promise;
readTOML(path: string): Promise;
readYAML(path: string): Promise;
ast(
path: string,
language: "typescript" | "javascript",
opts?: AstOptions
): Promise;
ast(
path: string,
language: "python",
opts?: AstOptions
): Promise;
ast(
path: string,
language: "ruby",
opts?: AstOptions
): Promise;
ast(path: string, language: AstLanguage, opts?: AstOptions): Promise;
findAstNodes(tree: EsTreeNode, ...types: string[]): EsTreeNode[];
findAstNodes(tree: PythonAstNode, ...types: string[]): PythonAstNode[];
findAstNodes(
tree: unknown,
...types: string[]
): (EsTreeNode | PythonAstNode)[];
checkCase(value: string, scheme: CaseScheme): boolean;
report: RuleReport;
}
```
### Properties
#### projectRoot
```typescript
projectRoot: string;
```
Absolute path to the project root directory (where `.archgate/` lives).
#### scopedFiles
```typescript
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
```typescript
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 [` 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
```typescript
report: RuleReport;
```
The reporting interface for recording violations, warnings, and informational messages. See [RuleReport](#rulereport) below.
### Methods
#### glob
```typescript
glob(pattern: string): Promise;
```
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.
```typescript
const testFiles = await ctx.glob("tests/**/*.test.ts");
```
#### grep
```typescript
grep(file: string, pattern: RegExp): Promise;
```
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.
```typescript
const matches = await ctx.grep(file, /console\.error\(/);
```
#### grepFiles
```typescript
grepFiles(pattern: RegExp, fileGlob: string): Promise;
```
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.
```typescript
const matches = await ctx.grepFiles(/TODO:/i, "src/**/*.ts");
```
#### readFile
```typescript
readFile(path: string): Promise;
```
Read the contents of a file as a string. The path is relative to the project root.
```typescript
const content = await ctx.readFile("src/config.ts");
```
#### fileAtBase
```typescript
fileAtBase(path: string): Promise;
```
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.
`archgate check` resolves the base ref itself, in this order: `--base ][`, then the base branch stored in `.archgate/config.json`, then auto-detection (`origin/HEAD`, `origin/main`/`origin/master`, then local `main`/`master`). A base is therefore normally available without passing any flag.
Returns `null` in the two "nothing to compare against" cases, so a single null check covers both:
- **No base is resolved** -- a `--staged` run, a project that is not a git repository, no base branch could be detected, or the histories are unrelated.
- **The file did not exist at the base** -- an added file.
```typescript
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
```typescript
readJSON(path: "package.json"): Promise;
readJSON(path: string): Promise;
```
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. The literal path `"package.json"` matches a dedicated overload that returns the ambient `PackageJson` interface (`name`, `version`, `scripts`, `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, `workspaces`, `engines`, ... plus an index signature), so the common case needs no cast.
Unlike `glob`, `readFile`, and `ast`, the parsed value is **not** cached across rules: each call returns a fresh object, so mutating it cannot leak into another rule.
```typescript
const pkg = await ctx.readJSON("package.json");
const deps = pkg.dependencies ?? {};
const tsconfig = (await ctx.readJSON("tsconfig.json")) as {
compilerOptions?: Record;
};
```
#### readTOML
```typescript
readTOML(path: string): Promise;
```
Read and parse a TOML file with Bun's built-in parser. The path is relative to the project root and uses the same sandbox as `readFile`. Any filename is accepted, including `.prototools`. The parsed value is `unknown`; narrow or cast it to your expected shape.
Each call returns a fresh parsed object, so mutations cannot leak into another call or rule. Invalid TOML and unreadable files throw, surfacing as a rule execution error with exit code 2.
```typescript
const doc = (await ctx.readTOML("pyproject.toml")) as {
project?: { dependencies?: string[] };
"dependency-groups"?: Record;
};
const dependencies = doc.project?.dependencies ?? [];
const workerDependencies = doc["dependency-groups"]?.worker ?? [];
```
Parsed values do not include source locations. Use `ctx.grep()` when a violation needs a line number; its matches include the line and column. A regex search finds text occurrences, so scope the search to the relevant table when keys repeat.
#### readYAML
```typescript
readYAML(path: string): Promise;
interface ReadYamlResult {
frontmatter: Record | null;
content: YamlValue;
}
type YamlValue =
| string
| number
| boolean
| null
| YamlValue[]
| { [key: string]: YamlValue };
```
Read a YAML file or a Markdown file with YAML frontmatter. The path is relative to the project root and passes through the same sandbox as `readFile`. One result object covers both shapes -- a nullable `frontmatter` mapping and a `content` typed as `YamlValue`, the JSON-like data YAML's core schema produces (after a `typeof` check you can index into mappings and sequences without casting) -- and dispatch is **extension-based**:
- **`.yml` / `.yaml` files**: the whole document is parsed as YAML. `frontmatter` is always `null`; `content` is the parsed value, narrowed by a `typeof` check rather than a cast -- unlike `readJSON`, which returns `unknown`. Because dispatch is by extension, a multi-document stream's `---` separators (Kubernetes manifests, CI configs) are never misread as frontmatter.
- **Every other file** (typically Markdown): the leading `---`-delimited block is parsed as `frontmatter` -- `null` when absent ("does this file have frontmatter?" is a single null test, mirroring `fileAtBase`), `{}` when present but empty. `content` is the remaining body text, trimmed -- it is **not** parsed as YAML.
`readYAML()` **throws** (fail-closed, like `ast()`) when a `.yml`/`.yaml` file is invalid YAML, or when a frontmatter block is invalid YAML or parses to something other than a mapping (a scalar or a sequence) -- surfacing as a rule execution error with exit code 2 rather than a false pass.
```typescript
const { content } = await ctx.readYAML(".github/workflows/ci.yml");
if (
typeof content === "object" &&
content !== null &&
!Array.isArray(content)
) {
const jobs = content.jobs; // YamlValue -- no cast needed
}
```
```typescript
for (const file of await ctx.glob("docs/**/*.md")) {
const { frontmatter } = await ctx.readYAML(file);
if (frontmatter === null) {
ctx.report.violation({ message: `${file} is missing frontmatter`, file });
continue;
}
if (typeof frontmatter.title !== "string") {
ctx.report.violation({
message: `${file} frontmatter must declare a title`,
file,
});
}
}
```
#### ast
```typescript
ast(
path: string,
language: "typescript" | "javascript",
opts?: AstOptions
): Promise;
ast(
path: string,
language: "python",
opts?: AstOptions
): Promise;
ast(
path: string,
language: "ruby",
opts?: AstOptions
): Promise;
ast(path: string, language: AstLanguage, opts?: AstOptions): Promise;
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`. Passing the language as a string literal selects a per-language overload, so the return type is already narrowed -- `program.body` is typed without a cast, and only a non-literal `AstLanguage` falls back to the `AstNode` union. 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](#astnode).
Parse results are cached for the lifetime of a single `archgate check` run, keyed on `(path, language, rev, comments)`: repeated identical calls -- across rules, or concurrent within one rule -- cost one parse (one interpreter spawn for Python/Ruby), and a failed parse rethrows the same error to every caller. Treat the returned tree as read-only -- it may be shared with other rules.
```typescript
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" }`)
With `{ rev: "base" }`, `ast()` parses the file at the [comparison base revision](#fileatbase) 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.)
```typescript
// 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 }`)
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 all four languages. For `ruby`, the returned tree is an array (`Ripper.sexp` output), so the `comments` array rides on it as a non-index property.
```typescript
interface CommentToken {
type: "line" | "block";
value: string; // delimiters (`//`, `/* */`, `#`) removed
loc: {
start: { line: number; column: number };
end: { line: number; column: number };
};
}
```
```typescript
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](#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. Ruby `#` comments are `type: "line"`; each `=begin`/`=end` documentation block is a single `type: "block"` token whose `value` is the inner content (the `=begin`/`=end` marker lines stripped, analogous to `/* */` delimiter stripping) and whose `loc` spans the `=begin` line through the `=end` line. Ruby comment `loc` columns are character offsets, consistent with the other languages (Ripper's own sexp node positions are byte offsets), and block `value` line endings are normalized to LF regardless of the source file's line endings. For Python and Ruby, comment extraction is a second pass over the same source (`tokenize` / `Ripper.lex`); a tokenizer failure on otherwise-parseable source degrades to an empty comment list rather than failing the parse. 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
`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`/`ruby` only): no suitable interpreter was found on `PATH`.
- **Implausible input**: the file's name does not match the requested language (e.g. `ctx.ast("config.json", "python")` throws before any interpreter is invoked). Accepted names are `.ts`/`.tsx`/`.mts`/`.cts` for `typescript`, `.js`/`.jsx`/`.mjs`/`.cjs` for `javascript`, `.py`/`.pyi` for `python`, and `.rb`/`.rake`/`.gemspec` plus the bare basenames `Rakefile` and `Gemfile` for `ruby`.
- **No base revision** (`{ rev: "base" }` only): no base ref resolved for this run (see [fileAtBase](#fileatbase)), so there is nothing to parse. Use `fileAtBase()` to detect this as `null` instead.
- **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".
:::caution
Python and Ruby rules require the corresponding interpreter (`python3`/`python`, `ruby`) on `PATH` wherever `archgate check` runs -- on every developer machine **and** in CI. TypeScript and JavaScript parsing is built into Archgate and needs no interpreter.
:::
#### findAstNodes
```typescript
findAstNodes(tree: EsTreeNode, ...types: string[]): EsTreeNode[];
findAstNodes(tree: PythonAstNode, ...types: string[]): PythonAstNode[];
findAstNodes(tree: unknown, ...types: string[]): (EsTreeNode | PythonAstNode)[];
```
Recursively collect every node in a parsed AST whose type-discriminant field matches one of `types`, in preorder (empty when nothing matches). This is the built-in replacement for the recursive walker AST rules used to hand-roll: `ctx.ast()` deliberately returns language-native shapes, but finding nodes by type name only depends on the discriminant field, so one helper covers all languages. Synchronous -- no `await` needed.
- **Language-agnostic**: each object node is checked against whichever discriminant field it carries -- `_type` (Python) or `type` (ESTree TypeScript/JavaScript).
- **Full traversal**: own-enumerable object values and arrays are recursed, and the `tree` argument itself is a match candidate.
- **Multi-type matching**: pass several names when one construct spans multiple node types -- the common case (`"FunctionDef"`/`"AsyncFunctionDef"`, sync/async variants).
- **Ruby**: `Ripper.sexp` nodes are plain arrays with no object discriminant field, so a Ruby tree is recursed but its array-shaped nodes never match -- walk Ripper output against its own grammar instead.
Before -- the collector each rule file had to repeat (rule files cannot import shared helper modules):
```typescript
function collectFunctionDefs(
node: unknown,
out: PythonAstNode[] = []
): PythonAstNode[] {
if (Array.isArray(node)) {
for (const item of node) collectFunctionDefs(item, out);
return out;
}
if (!node || typeof node !== "object") return out;
const n = node as PythonAstNode;
if (n._type === "FunctionDef" || n._type === "AsyncFunctionDef") out.push(n);
for (const value of Object.values(n)) {
if (value && typeof value === "object") collectFunctionDefs(value, out);
}
return out;
}
const tree = await ctx.ast("app/models.py", "python");
const funcDefs = collectFunctionDefs(tree);
```
After:
```typescript
const tree = await ctx.ast("app/models.py", "python");
const funcDefs = ctx.findAstNodes(tree, "FunctionDef", "AsyncFunctionDef");
```
#### checkCase
```typescript
checkCase(value: string, scheme: CaseScheme): boolean;
type CaseScheme =
| "kebab-case"
| "camelCase"
| "PascalCase"
| "snake_case"
| "SCREAMING_SNAKE_CASE";
```
Check whether a string conforms to a casing scheme -- the built-in replacement for the per-scheme regexes naming rules used to hand-roll. Synchronous and pure -- no `await` needed. Matching is all-or-nothing (the entire string must conform; the empty string matches no scheme) and the vocabulary is ASCII letters and digits only.
| Scheme | Matches | Rejects |
| ---------------------- | ---------------------------- | -------------------------------------------------- |
| `kebab-case` | `writing-rules`, `2fa-setup` | `Writing-Rules`, `writing_rules`, `writing--rules` |
| `camelCase` | `checkCase`, `parseURL` | `CheckCase`, `check_case`, `2fast` |
| `PascalCase` | `CheckCase`, `HTTPServer` | `checkCase`, `Check_Case`, `1Value` |
| `snake_case` | `check_case`, `2fa_setup` | `Check_Case`, `check-case`, `check__case` |
| `SCREAMING_SNAKE_CASE` | `CHECK_CASE`, `V2` | `check_case`, `CHECK-CASE`, `CHECK__CASE` |
`camelCase` and `PascalCase` follow the ecosystem convention (typescript-eslint's `naming-convention`): a leading lower/uppercase letter followed by any ASCII alphanumerics, so acronym runs (`parseURL`, `HTTPServer`) match. Degenerate values can satisfy several schemes at once (`value` is valid kebab-case, snake_case, and camelCase). Passing an unrecognized scheme name **throws** rather than silently returning `false`, so a typo surfaces as a rule error instead of a false pass/fail.
```typescript
for (const file of await ctx.glob("docs/**/*.md")) {
const stem = file.split("/").pop()?.replace(/\.md$/, "") ?? "";
if (!ctx.checkCase(stem, "kebab-case")) {
ctx.report.violation({
message: `${file} must have a kebab-case filename`,
file,
});
}
}
```
---
## RuleReport
The reporting interface for recording check results. Each method accepts a detail object describing the issue.
```typescript
interface RuleReport {
violation(detail: ReportDetail): void;
warning(detail: ReportDetail): void;
info(detail: ReportDetail): void;
}
```
#### violation
```typescript
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
```typescript
report.warning(detail: ReportDetail): void;
```
Report a warning. Warnings appear in check output but do not cause the check to fail -- unless `archgate check --strict` is used, which turns any warning into a failure (exit 1). Use for non-blocking guidance.
#### info
```typescript
report.info(detail: ReportDetail): void;
```
Report an informational message. Does not affect the check exit code. Use for suggestions or notes.
### ReportDetail
The detail object passed to `violation`, `warning`, and `info`.
```typescript
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 range highlighting |
| `endColumn` | `number` | No | End column number (0-based), for range highlighting |
| `fix` | `string` | No | Suggested fix or remediation action |
Which reporter surfaces which field:
- **Console** prints `message` and `file:line`. It prints `fix` only under `--verbose`, and never columns.
- **`--output json`** carries every field above, so agents and editor integrations can highlight the exact range instead of the whole line.
- **`--output sarif`** carries `message`, `file`, `line`, and `endLine` (as a SARIF `region`). Columns and `fix` are omitted -- Archgate's report detail carries an `endColumn` but no `startColumn` to anchor it to, so the serializer emits only the line fields.
- **`--output github`** emits `file`/`line` annotations with `message` only.
Put anything a reviewer must read in `message`; treat `fix` as an enrichment for local and agent-facing output.
---
## GrepMatch
Returned by `ctx.grep()` and `ctx.grepFiles()`.
```typescript
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
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.
```typescript
type AstLanguage = "typescript" | "javascript" | "python" | "ruby";
/** Only the fields every node shares are typed; walk the rest through the index signature. */
interface EsTreeNode {
type: string;
loc?: {
start: { line: number; column: number };
end: { line: number; column: number };
} | null;
range?: [number, number];
[key: string]: unknown;
}
interface EsTreeProgram extends EsTreeNode {
type: "Program";
sourceType: "module" | "script";
body: EsTreeNode[];
comments?: CommentToken[];
}
interface PythonAstNode {
_type: string;
lineno?: number;
col_offset?: number;
end_lineno?: number;
end_col_offset?: number;
[key: string]: unknown;
}
interface PythonAstModule extends PythonAstNode {
_type: "Module";
body: PythonAstNode[];
comments?: CommentToken[];
}
type RubyAstNode = unknown[];
interface RubyAstProgram extends Array {
comments?: CommentToken[];
}
/** Only the return type of the non-literal-language overload. */
type AstNode = EsTreeProgram | PythonAstModule | RubyAstProgram;
```
All of these names are ambient in `.rules.ts` files through the `rules.d.ts` reference -- no import is needed to annotate a walker with `EsTreeNode` or `PythonAstNode`.
When parsed with [`{ comments: true }`](#ast), the root node also carries a `comments: CommentToken[]` array (all four languages; for `ruby` it is a non-index property on the root sexp array). It is absent otherwise.
| Language | Backing parser | Returned shape |
| ------------ | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `typescript` | [meriyah](https://github.com/meriyah/meriyah), in-process, after transpiling TypeScript away with `Bun.Transpiler` | [ESTree](https://github.com/estree/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](https://github.com/meriyah/meriyah), in-process | [ESTree](https://github.com/estree/estree) `Program` with `loc` position info |
| `python` | Python's standard-library [`ast` module](https://docs.python.org/3/library/ast.html), 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`](https://docs.ruby-lang.org/en/master/Ripper.html), 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()](/guides/writing-rules/#structural-checks-with-ctxast) for a complete example rule per language.
---
## Severity
```typescript
type Severity = "error" | "warning" | "info";
```
| Value | Reported by | Exit code impact | Description |
| ----------- | ------------------------ | --------------------------------- | ------------------------------- |
| `"error"` | `ctx.report.violation()` | Causes exit 1 | Hard constraint, blocks merges |
| `"warning"` | `ctx.report.warning()` | None, but exit 1 under `--strict` | Non-blocking guidance |
| `"info"` | `ctx.report.info()` | None | Informational, suggestions only |
A rule that throws (or exceeds the 30-second rule timeout) is a rule execution error, which exits 2 rather than 1.
---
## ViolationDetail
The internal representation of a reported issue, used in check output and JSON results.
```typescript
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
Violations can be suppressed in source code using `archgate-ignore` comments. The engine handles this automatically. Rules do not need any special logic.
```typescript
// archgate-ignore ARCH-006/no-unapproved-deps legacy dep, migration planned
import 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](/guides/writing-rules/#opt-out-directives) for full details and custom directive patterns.
]