--- 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.