From d1e4795cdcb27a6e15b89f7a81fa7675485f813e Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 23 Mar 2026 16:11:12 +0100 Subject: [PATCH 1/2] fix: include blocked rules in JSON output with range info When a rule file is blocked by the security scanner, the CLI now includes it in the JSON output as a RuleResult with status "error" instead of throwing and producing no JSON. Violations include file, line, endLine, and endColumn so editors can highlight the exact blocked expression (e.g., Bun.spawn()). Normal ADR rule violations can also report endLine/endColumn for precise range highlighting. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../docs/pt-br/reference/cli-commands.mdx | 51 ++++++-- .../content/docs/pt-br/reference/rule-api.mdx | 40 +++--- .../content/docs/reference/cli-commands.mdx | 51 ++++++-- docs/src/content/docs/reference/rule-api.mdx | 40 +++--- src/commands/check.ts | 8 +- src/engine/context.ts | 6 +- src/engine/loader.ts | 115 ++++++++++++++++-- src/engine/reporter.ts | 4 + src/engine/rule-scanner.ts | 7 +- src/engine/runner.ts | 15 ++- src/formats/rules.ts | 2 + src/helpers/rules-shim.ts | 2 + tests/engine/loader-security.test.ts | 26 +++- tests/engine/loader.test.ts | 20 ++- tests/engine/runner-security.test.ts | 27 ++-- tests/engine/runner.test.ts | 27 ++-- 16 files changed, 337 insertions(+), 104 deletions(-) diff --git a/docs/src/content/docs/pt-br/reference/cli-commands.mdx b/docs/src/content/docs/pt-br/reference/cli-commands.mdx index 2bcfed77..b72b0d17 100644 --- a/docs/src/content/docs/pt-br/reference/cli-commands.mdx +++ b/docs/src/content/docs/pt-br/reference/cli-commands.mdx @@ -275,10 +275,10 @@ archgate plugin install --editor cursor Executa todas as verificações automatizadas de conformidade com ADRs no codebase. ```bash -archgate check [options] +archgate check [options] [files...] ``` -Carrega cada ADR com `rules: true` no frontmatter, executa o arquivo `.rules.ts` complementar e reporta violações com caminhos de arquivo e números de linha. +Carrega cada ADR com `rules: true` no frontmatter, executa o arquivo `.rules.ts` complementar e reporta violações com caminhos de arquivo e números de linha. Quando caminhos de arquivo são fornecidos como argumentos posicionais, apenas ADRs cujos padrões `files` correspondem a esses arquivos são executados. ### Opções @@ -290,13 +290,19 @@ Carrega cada ADR com `rules: true` no frontmatter, executa o arquivo `.rules.ts` | `--adr ` | Verificar apenas regras de um ADR específico | | `--verbose` | Mostrar regras aprovadas e informações de tempo | +### Argumentos + +| Argumento | Descrição | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[files...]` | Caminhos de arquivo opcionais para limitar as verificações. Apenas ADRs cujos padrões `files` correspondem serão executados. Suporta pipe via stdin. | + ### Códigos de saída -| Código | Significado | -| ------ | ------------------------------------------------------ | -| 0 | Todas as regras passaram. Nenhuma violação encontrada. | -| 1 | Uma ou mais violações detectadas. | -| 2 | Erro interno (ex.: ADR ou regra malformada). | +| Código | Significado | +| ------ | ------------------------------------------------------------------------------------ | +| 0 | Todas as regras passaram. Nenhuma violação encontrada. | +| 1 | Uma ou mais violações detectadas. | +| 2 | Erro na execução de regra (ex.: regra malformada, bloqueio do scanner de segurança). | ### Exemplos @@ -318,6 +324,18 @@ Verificar um único ADR: archgate check --adr ARCH-001 ``` +Verificar arquivos específicos (apenas ADRs correspondentes são executados): + +```bash +archgate check src/foo.ts src/bar.ts +``` + +Pipe do git (verificar apenas arquivos alterados): + +```bash +git diff --name-only | archgate check --json +``` + Obter saída JSON para integração com CI: ```bash @@ -357,6 +375,9 @@ Quando `--json` é usado, a saída é um único objeto JSON: { "message": "Command file must export a register*Command function", "file": "src/commands/broken.ts", + "line": 1, + "endLine": 1, + "endColumn": 42, "severity": "error" } ], @@ -367,6 +388,22 @@ Quando `--json` é usado, a saída é um único objeto JSON: } ``` +#### Campos de violação + +| Campo | Tipo | Descrição | +| ----------- | ------- | ------------------------------------------------------- | +| `message` | string | Descrição da violação | +| `file` | string? | Caminho relativo do arquivo | +| `line` | number? | Linha inicial (base 1) | +| `endLine` | number? | Linha final (base 1) — para destaque preciso no editor | +| `endColumn` | number? | Coluna final (base 0) — para destaque preciso no editor | +| `fix` | string? | Correção sugerida (apenas orientação) | +| `severity` | string | `"error"`, `"warning"` ou `"info"` | + +#### Arquivos de regra bloqueados + +Quando um arquivo de regra é bloqueado pelo scanner de segurança (ex.: usa `Bun.spawn()`) ou um arquivo `.rules.ts` complementar está ausente, o resultado aparece na saída JSON com `status: "error"` e `ruleId: "security-scan"`. As violações incluem o arquivo e a linha exata do código bloqueado (ou a linha `rules: true` no ADR para arquivos complementares ausentes). + --- ## archgate adr create diff --git a/docs/src/content/docs/pt-br/reference/rule-api.mdx b/docs/src/content/docs/pt-br/reference/rule-api.mdx index 955d3154..1f675453 100644 --- a/docs/src/content/docs/pt-br/reference/rule-api.mdx +++ b/docs/src/content/docs/pt-br/reference/rule-api.mdx @@ -214,16 +214,22 @@ interface ReportDetail { message: string; file?: string; line?: number; + endLine?: number; + endColumn?: number; fix?: string; } ``` -| Campo | Tipo | Obrigatório | Descrição | -| --------- | -------- | ----------- | ------------------------------------------------- | -| `message` | `string` | Sim | Descrição legível do problema | -| `file` | `string` | Não | Caminho do arquivo onde o problema foi encontrado | -| `line` | `number` | Não | Número da linha no arquivo | -| `fix` | `string` | Não | Sugestão de correção ou ação de remediação | +| Campo | Tipo | Obrigatório | Descrição | +| ----------- | -------- | ----------- | ----------------------------------------------------------------- | +| `message` | `string` | Sim | Descrição legível do problema | +| `file` | `string` | Não | Caminho do arquivo onde o problema foi encontrado | +| `line` | `number` | Não | Número da linha inicial (base 1) | +| `endLine` | `number` | Não | Número da linha final (base 1) — para destaque preciso no editor | +| `endColumn` | `number` | Não | Número da coluna final (base 0) — para destaque preciso no editor | +| `fix` | `string` | Não | Sugestão de correção ou ação de remediação | + +Quando `endLine` e `endColumn` são fornecidos, editores (VS Code, Cursor) podem destacar a expressão exata que viola a regra, em vez da linha inteira. Se omitidos, a linha completa em `line` é destacada. --- @@ -274,20 +280,24 @@ interface ViolationDetail { message: string; file?: string; line?: number; + endLine?: number; + endColumn?: number; fix?: string; severity: Severity; } ``` -| Campo | Tipo | Descrição | -| ---------- | ---------- | ------------------------------------------------- | -| `ruleId` | `string` | ID da regra da chave do objeto `rules` | -| `adrId` | `string` | ID do ADR do frontmatter | -| `message` | `string` | Descrição legível | -| `file` | `string?` | Caminho do arquivo onde o problema foi encontrado | -| `line` | `number?` | Número da linha no arquivo | -| `fix` | `string?` | Sugestão de correção | -| `severity` | `Severity` | Severidade efetiva desta violação | +| Campo | Tipo | Descrição | +| ----------- | ---------- | ------------------------------------------------------- | +| `ruleId` | `string` | ID da regra da chave do objeto `rules` | +| `adrId` | `string` | ID do ADR do frontmatter | +| `message` | `string` | Descrição legível | +| `file` | `string?` | Caminho do arquivo onde o problema foi encontrado | +| `line` | `number?` | Número da linha inicial (base 1) | +| `endLine` | `number?` | Linha final (base 1) — para destaque preciso no editor | +| `endColumn` | `number?` | Coluna final (base 0) — para destaque preciso no editor | +| `fix` | `string?` | Sugestão de correção | +| `severity` | `Severity` | Severidade efetiva desta violação | --- diff --git a/docs/src/content/docs/reference/cli-commands.mdx b/docs/src/content/docs/reference/cli-commands.mdx index e6b169ef..e4d93872 100644 --- a/docs/src/content/docs/reference/cli-commands.mdx +++ b/docs/src/content/docs/reference/cli-commands.mdx @@ -277,10 +277,10 @@ archgate plugin install --editor cursor Run all automated ADR compliance checks against the codebase. ```bash -archgate check [options] +archgate check [options] [files...] ``` -Loads every ADR with `rules: true` in its frontmatter, executes the companion `.rules.ts` file, and reports violations with file paths and line numbers. +Loads every ADR with `rules: true` in its frontmatter, executes the companion `.rules.ts` file, and reports violations with file paths and line numbers. When file paths are provided as positional arguments, only ADRs whose `files` patterns match those files are executed. ### Options @@ -292,13 +292,19 @@ Loads every ADR with `rules: true` in its frontmatter, executes the companion `. | `--adr ` | Only check rules from a specific ADR | | `--verbose` | Show passing rules and timing info | +### Arguments + +| Argument | Description | +| ------------ | --------------------------------------------------------------------------------------------------------------- | +| `[files...]` | Optional file paths to scope checks to. Only ADRs whose `files` patterns match will run. Supports stdin piping. | + ### Exit codes -| Code | Meaning | -| ---- | --------------------------------------------- | -| 0 | All rules pass. No violations found. | -| 1 | One or more violations detected. | -| 2 | Internal error (e.g., malformed ADR or rule). | +| Code | Meaning | +| ---- | -------------------------------------------------------------------- | +| 0 | All rules pass. No violations found. | +| 1 | One or more violations detected. | +| 2 | Rule execution error (e.g., malformed rule, security scanner block). | ### Examples @@ -320,6 +326,18 @@ Check a single ADR: archgate check --adr ARCH-001 ``` +Check specific files (only matching ADRs run): + +```bash +archgate check src/foo.ts src/bar.ts +``` + +Pipe from git (check only changed files): + +```bash +git diff --name-only | archgate check --json +``` + Get JSON output for CI integration: ```bash @@ -359,6 +377,9 @@ When `--json` is used, the output is a single JSON object: { "message": "Command file must export a register*Command function", "file": "src/commands/broken.ts", + "line": 1, + "endLine": 1, + "endColumn": 42, "severity": "error" } ], @@ -369,6 +390,22 @@ When `--json` is used, the output is a single JSON object: } ``` +#### Violation fields + +| Field | Type | Description | +| ----------- | ------- | ------------------------------------------------------ | +| `message` | string | What the violation is | +| `file` | string? | Relative file path | +| `line` | number? | Start line (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 (guidance only) | +| `severity` | string | `"error"`, `"warning"`, or `"info"` | + +#### Blocked rule files + +When a rule file is blocked by the security scanner (e.g., uses `Bun.spawn()`) or a companion `.rules.ts` file is missing, the result appears in the JSON output with `status: "error"` and `ruleId: "security-scan"`. Violations include the exact file and line of the blocked code (or the `rules: true` line in the ADR for missing companions). + --- ## archgate adr create diff --git a/docs/src/content/docs/reference/rule-api.mdx b/docs/src/content/docs/reference/rule-api.mdx index 85ba116e..d260908f 100644 --- a/docs/src/content/docs/reference/rule-api.mdx +++ b/docs/src/content/docs/reference/rule-api.mdx @@ -214,16 +214,22 @@ 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 | Line number in the file | -| `fix` | `string` | No | Suggested fix or remediation action | +| 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. --- @@ -274,20 +280,24 @@ interface ViolationDetail { 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?` | Line number in the file | -| `fix` | `string?` | Suggested fix | -| `severity` | `Severity` | Effective severity of this violation | +| 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 | --- diff --git a/src/commands/check.ts b/src/commands/check.ts index 5706056e..724aa276 100644 --- a/src/commands/check.ts +++ b/src/commands/check.ts @@ -31,9 +31,9 @@ export function registerCheckCommand(program: Command) { process.exit(1); } - let loadedAdrs; + let loadResults; try { - loadedAdrs = await loadRuleAdrs(projectRoot, opts.adr); + loadResults = await loadRuleAdrs(projectRoot, opts.adr); } catch (err) { logError( err instanceof Error ? err.message : `Failed to load rules: ${err}` @@ -43,7 +43,7 @@ export function registerCheckCommand(program: Command) { const useJson = opts.json || (!opts.ci && isAgentContext()); - if (loadedAdrs.length === 0) { + if (loadResults.length === 0) { if (useJson) { console.log( formatJSON( @@ -78,7 +78,7 @@ export function registerCheckCommand(program: Command) { filterFiles = [...filterFiles, ...piped]; } - const result = await runChecks(projectRoot, loadedAdrs, { + const result = await runChecks(projectRoot, loadResults, { staged: opts.staged, files: filterFiles.length > 0 ? filterFiles : undefined, }); diff --git a/src/engine/context.ts b/src/engine/context.ts index 878a0e5e..b831fa99 100644 --- a/src/engine/context.ts +++ b/src/engine/context.ts @@ -245,9 +245,9 @@ export async function buildReviewContext( let checkSummary: ReportSummary | null = null; if (options?.runChecks) { - const loadedAdrs = await loadRuleAdrs(projectRoot); - if (loadedAdrs.length > 0) { - const checkResult = await runChecks(projectRoot, loadedAdrs, { staged }); + const loadResults = await loadRuleAdrs(projectRoot); + if (loadResults.length > 0) { + const checkResult = await runChecks(projectRoot, loadResults, { staged }); checkSummary = buildSummary(checkResult, { maxViolationsPerRule }); } else { checkSummary = { ...EMPTY_SUMMARY }; diff --git a/src/engine/loader.ts b/src/engine/loader.ts index f9ee88f2..841d4677 100644 --- a/src/engine/loader.ts +++ b/src/engine/loader.ts @@ -22,6 +22,9 @@ const RuleSetSchema = z.object({ }) ), }); +import { relative } from "node:path"; + +import type { ViolationDetail } from "../formats/rules"; import { logDebug, logError } from "../helpers/log"; import { projectPaths } from "../helpers/paths"; import { ensureRulesShim } from "../helpers/rules-shim"; @@ -32,13 +35,54 @@ export interface LoadedAdr { ruleSet: RuleSet; } +export interface BlockedAdr { + adr: AdrDocument; + error: string; + violations: Array<{ + message: string; + file: string; + line: number; + column: number; + endLine: number; + endColumn: number; + }>; +} + +export type LoadResult = + | { type: "loaded"; value: LoadedAdr } + | { type: "blocked"; value: BlockedAdr }; + +/** Convert a BlockedAdr into a RuleResult-shaped object for reporting. */ +export function blockedToRuleResult(projectRoot: string, b: BlockedAdr) { + const id = b.adr.frontmatter.id; + return { + ruleId: "security-scan", + adrId: id, + description: "Rule file security scan", + violations: b.violations.map( + (v): ViolationDetail => ({ + message: v.message, + file: relative(projectRoot, v.file).replaceAll("\\", "/"), + line: v.line, + endLine: v.endLine, + endColumn: v.endColumn, + severity: "error", + adrId: id, + ruleId: "security-scan", + }) + ), + error: b.error, + durationMs: 0, + }; +} + /** * Discover ADRs with rules: true and dynamically import their companion .rules.ts files. */ export async function loadRuleAdrs( projectRoot: string, filterAdrId?: string -): Promise { +): Promise { const pp = projectPaths(projectRoot); // Ensure rules.d.ts exists so .rules.ts files get type checking @@ -80,15 +124,43 @@ export async function loadRuleAdrs( // Phase 2: Verify companion files exist and import rule sets in parallel const ruleResults = await Promise.all( - ruleAdrs.map(async ({ file, adr }) => { + ruleAdrs.map(async ({ file, adr }): Promise => { const baseName = basename(file, ".md"); const rulesFile = join(adrsDir, `${baseName}.rules.ts`); const rulesFileExists = await Bun.file(rulesFile).exists(); if (!rulesFileExists) { - throw new Error( - `ADR ${adr.frontmatter.id} has rules: true but no companion file found: ${rulesFile}` - ); + // Find the "rules: true" line in the ADR file for precise highlighting + const adrPath = join(adrsDir, file); + const adrContent = await Bun.file(adrPath).text(); + const adrLines = adrContent.split("\n"); + let rulesLine = 1; + let rulesEndCol = 0; + for (let i = 0; i < adrLines.length; i++) { + const match = adrLines[i].match(/^rules:\s*true/); + if (match) { + rulesLine = i + 1; + rulesEndCol = adrLines[i].length; + break; + } + } + return { + type: "blocked", + value: { + adr, + error: `ADR ${adr.frontmatter.id} has rules: true but no companion file found`, + violations: [ + { + message: `No companion .rules.ts file found. Create ${baseName}.rules.ts or set rules: false.`, + file: adrPath, + line: rulesLine, + column: 0, + endLine: rulesLine, + endColumn: rulesEndCol, + }, + ], + }, + }; } // Security gate: scan rule source for banned patterns before executing. @@ -101,9 +173,21 @@ export async function loadRuleAdrs( for (const v of scanViolations) { logError(`${rulesFile}:${v.line}:${v.column} - ${v.message}`); } - throw new Error( - `ADR ${adr.frontmatter.id}: rule file blocked by security scanner (${scanViolations.length} violation${scanViolations.length === 1 ? "" : "s"})` - ); + return { + type: "blocked", + value: { + adr, + error: `ADR ${adr.frontmatter.id}: rule file blocked by security scanner (${scanViolations.length} violation${scanViolations.length === 1 ? "" : "s"})`, + violations: scanViolations.map((v) => ({ + message: v.message, + file: rulesFile, + line: v.line, + column: v.column, + endLine: v.endLine, + endColumn: v.endColumn, + })), + }, + }; } // Cache-bust: Bun caches import() per-process, so append a timestamp @@ -114,18 +198,23 @@ export async function loadRuleAdrs( const parsed = RuleSetSchema.safeParse(mod.default); if (!parsed.success) { - throw new Error( - `ADR ${adr.frontmatter.id}: companion file does not export a valid RuleSet as default` - ); + return { + type: "blocked", + value: { + adr, + error: `ADR ${adr.frontmatter.id}: companion file does not export a valid RuleSet as default`, + violations: [], + }, + }; } const ruleSet: RuleSet = parsed.data; logDebug( `Loaded ${Object.keys(ruleSet.rules).length} rules from ${adr.frontmatter.id}` ); - return { adr, ruleSet }; + return { type: "loaded", value: { adr, ruleSet } }; }) ); - return ruleResults.filter((r): r is LoadedAdr => r !== null); + return ruleResults; } diff --git a/src/engine/reporter.ts b/src/engine/reporter.ts index 43cd67ee..3e88b93a 100644 --- a/src/engine/reporter.ts +++ b/src/engine/reporter.ts @@ -25,6 +25,8 @@ export interface ReportSummary { message: string; file?: string; line?: number; + endLine?: number; + endColumn?: number; fix?: string; severity: Severity; }>; @@ -91,6 +93,8 @@ export function buildSummary( message: v.message, file: v.file, line: v.line, + endLine: v.endLine, + endColumn: v.endColumn, fix: v.fix, severity: v.severity, })), diff --git a/src/engine/rule-scanner.ts b/src/engine/rule-scanner.ts index 334fc256..5d473c5f 100644 --- a/src/engine/rule-scanner.ts +++ b/src/engine/rule-scanner.ts @@ -16,10 +16,13 @@ export interface ScanViolation { message: string; line: number; column: number; + endLine: number; + endColumn: number; } interface AstLoc { start: { line: number; column: number }; + end: { line: number; column: number }; } interface AstNode { @@ -28,10 +31,12 @@ interface AstNode { [key: string]: unknown; } -function loc(node: AstNode): { line: number; column: number } { +function loc(node: AstNode) { return { line: node.loc?.start.line ?? 0, column: node.loc?.start.column ?? 0, + endLine: node.loc?.end.line ?? 0, + endColumn: node.loc?.end.column ?? 0, }; } diff --git a/src/engine/runner.ts b/src/engine/runner.ts index 11540add..54234bc4 100644 --- a/src/engine/runner.ts +++ b/src/engine/runner.ts @@ -9,6 +9,7 @@ import type { } from "../formats/rules"; import { logDebug } from "../helpers/log"; import { resolveScopedFiles, getStagedFiles } from "./git-files"; +import { type LoadResult, blockedToRuleResult } from "./loader"; /** * Resolve a user-supplied path and ensure it stays within projectRoot. @@ -53,8 +54,6 @@ function safeGlob(pattern: string): void { throw new Error(`Glob pattern "${pattern}" is absolute — access denied`); } } -import type { LoadedAdr } from "./loader"; - const RULE_TIMEOUT_MS = 30_000; export interface RuleResult { @@ -179,12 +178,20 @@ function createRuleContext( */ export async function runChecks( projectRoot: string, - loadedAdrs: LoadedAdr[], + loadResults: LoadResult[], options: { staged?: boolean; files?: string[] } = {} ): Promise { const startTime = performance.now(); const changedFiles = options.staged ? await getStagedFiles(projectRoot) : []; - const results: RuleResult[] = []; + const results: RuleResult[] = loadResults + .filter((lr) => lr.type === "blocked") + .map((lr) => blockedToRuleResult(projectRoot, lr.value)); + const loadedAdrs = loadResults + .filter( + (lr): lr is Extract => + lr.type === "loaded" + ) + .map((lr) => lr.value); // Resolve user-specified files to relative paths for intersection let filterFiles: Set | undefined; diff --git a/src/formats/rules.ts b/src/formats/rules.ts index 37804159..8cb2600b 100644 --- a/src/formats/rules.ts +++ b/src/formats/rules.ts @@ -19,6 +19,8 @@ export interface ViolationDetail { message: string; file?: string; line?: number; + endLine?: number; + endColumn?: number; fix?: string; severity: Severity; } diff --git a/src/helpers/rules-shim.ts b/src/helpers/rules-shim.ts index 1345f927..39f8bcf5 100644 --- a/src/helpers/rules-shim.ts +++ b/src/helpers/rules-shim.ts @@ -24,6 +24,8 @@ declare interface ViolationDetail { message: string; file?: string; line?: number; + endLine?: number; + endColumn?: number; fix?: string; severity: Severity; } diff --git a/tests/engine/loader-security.test.ts b/tests/engine/loader-security.test.ts index 8a82bf25..0f912122 100644 --- a/tests/engine/loader-security.test.ts +++ b/tests/engine/loader-security.test.ts @@ -55,7 +55,10 @@ export default { ` ); - await expect(loadRuleAdrs(tempDir)).rejects.toThrow( + const results = await loadRuleAdrs(tempDir); + expect(results).toHaveLength(1); + expect(results[0].type).toBe("blocked"); + expect((results[0] as { value: { error: string } }).value.error).toContain( "blocked by security scanner" ); }); @@ -78,7 +81,10 @@ export default { ` ); - await expect(loadRuleAdrs(tempDir)).rejects.toThrow( + const results = await loadRuleAdrs(tempDir); + expect(results).toHaveLength(1); + expect(results[0].type).toBe("blocked"); + expect((results[0] as { value: { error: string } }).value.error).toContain( "blocked by security scanner" ); }); @@ -101,7 +107,10 @@ export default { ` ); - await expect(loadRuleAdrs(tempDir)).rejects.toThrow( + const results = await loadRuleAdrs(tempDir); + expect(results).toHaveLength(1); + expect(results[0].type).toBe("blocked"); + expect((results[0] as { value: { error: string } }).value.error).toContain( "blocked by security scanner" ); }); @@ -124,7 +133,10 @@ export default { ` ); - await expect(loadRuleAdrs(tempDir)).rejects.toThrow( + const results = await loadRuleAdrs(tempDir); + expect(results).toHaveLength(1); + expect(results[0].type).toBe("blocked"); + expect((results[0] as { value: { error: string } }).value.error).toContain( "blocked by security scanner" ); }); @@ -155,7 +167,11 @@ export default { const loaded = await loadRuleAdrs(tempDir); expect(loaded).toHaveLength(1); - expect(loaded[0].adr.frontmatter.id).toBe("SEC-005"); + expect(loaded[0].type).toBe("loaded"); + expect( + (loaded[0] as { value: { adr: { frontmatter: { id: string } } } }).value + .adr.frontmatter.id + ).toBe("SEC-005"); }); test("allows rule with safe imports (node:path)", async () => { diff --git a/tests/engine/loader.test.ts b/tests/engine/loader.test.ts index 34a49743..114195d1 100644 --- a/tests/engine/loader.test.ts +++ b/tests/engine/loader.test.ts @@ -50,8 +50,10 @@ describe("loadRuleAdrs", () => { const loaded = await loadRuleAdrs(tempDir); expect(loaded).toHaveLength(1); - expect(loaded[0].adr.frontmatter.id).toBe("TEST-001"); - expect(Object.keys(loaded[0].ruleSet.rules)).toEqual(["sample-rule"]); + expect(loaded[0].type).toBe("loaded"); + const first = loaded[0] as Extract<(typeof loaded)[0], { type: "loaded" }>; + expect(first.value.adr.frontmatter.id).toBe("TEST-001"); + expect(Object.keys(first.value.ruleSet.rules)).toEqual(["sample-rule"]); }); test("skips ADR with rules: false", async () => { @@ -64,15 +66,21 @@ describe("loadRuleAdrs", () => { expect(loaded).toHaveLength(0); }); - test("throws when companion file is missing", () => { + test("returns blocked result when companion file is missing", async () => { copyFileSync( join(fixturesDir, "TEST-004-missing-companion.md"), join(tempDir, ".archgate", "adrs", "TEST-004-missing-companion.md") ); - expect(loadRuleAdrs(tempDir)).rejects.toThrow( - "has rules: true but no companion file found" - ); + const results = await loadRuleAdrs(tempDir); + expect(results).toHaveLength(1); + expect(results[0].type).toBe("blocked"); + const blocked = results[0] as Extract< + (typeof results)[0], + { type: "blocked" } + >; + expect(blocked.value.error).toContain("no companion file found"); + expect(blocked.value.violations).toHaveLength(1); }); test("filters by ADR ID", async () => { diff --git a/tests/engine/runner-security.test.ts b/tests/engine/runner-security.test.ts index 4c125a90..ea4bf992 100644 --- a/tests/engine/runner-security.test.ts +++ b/tests/engine/runner-security.test.ts @@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { LoadedAdr } from "../../src/engine/loader"; +import type { LoadResult } from "../../src/engine/loader"; import { runChecks } from "../../src/engine/runner"; import type { AdrDocument } from "../../src/formats/adr"; import type { RuleSet } from "../../src/formats/rules"; @@ -25,20 +25,23 @@ describe("runChecks path sandboxing", () => { function makeLoadedAdr( overrides: Partial = {}, ruleSet: RuleSet = EMPTY_RULE_SET - ): LoadedAdr { + ): LoadResult { return { - adr: { - frontmatter: { - id: "SEC-001", - title: "Security Test", - domain: "general", - rules: true, - ...overrides, + type: "loaded", + value: { + adr: { + frontmatter: { + id: "SEC-001", + title: "Security Test", + domain: "general", + rules: true, + ...overrides, + }, + body: "", + filePath: "/test.md", }, - body: "", - filePath: "/test.md", + ruleSet, }, - ruleSet, }; } diff --git a/tests/engine/runner.test.ts b/tests/engine/runner.test.ts index e0382a9e..3ec79fdb 100644 --- a/tests/engine/runner.test.ts +++ b/tests/engine/runner.test.ts @@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { LoadedAdr } from "../../src/engine/loader"; +import type { LoadResult } from "../../src/engine/loader"; import { runChecks } from "../../src/engine/runner"; import type { AdrDocument } from "../../src/formats/adr"; import type { RuleSet } from "../../src/formats/rules"; @@ -25,20 +25,23 @@ describe("runChecks", () => { function makeLoadedAdr( overrides: Partial = {}, ruleSet: RuleSet = EMPTY_RULE_SET - ): LoadedAdr { + ): LoadResult { return { - adr: { - frontmatter: { - id: "TEST-001", - title: "Test", - domain: "general", - rules: true, - ...overrides, + type: "loaded", + value: { + adr: { + frontmatter: { + id: "TEST-001", + title: "Test", + domain: "general", + rules: true, + ...overrides, + }, + body: "", + filePath: "/test.md", }, - body: "", - filePath: "/test.md", + ruleSet, }, - ruleSet, }; } From c64bea3adfcd8190fb1c23ec2597d157146ac80e Mon Sep 17 00:00:00 2001 From: rhuanbarreto <283004+rhuanbarreto@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:38:03 +0000 Subject: [PATCH 2/2] docs: regenerate llms-full.txt --- docs/public/llms-full.txt | 91 +++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 22 deletions(-) diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index 9bee6269..076744e5 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -3143,10 +3143,10 @@ archgate plugin install --editor cursor Run all automated ADR compliance checks against the codebase. ```bash -archgate check [options] +archgate check [options] [files...] ``` -Loads every ADR with `rules: true` in its frontmatter, executes the companion `.rules.ts` file, and reports violations with file paths and line numbers. +Loads every ADR with `rules: true` in its frontmatter, executes the companion `.rules.ts` file, and reports violations with file paths and line numbers. When file paths are provided as positional arguments, only ADRs whose `files` patterns match those files are executed. ### Options @@ -3158,13 +3158,19 @@ Loads every ADR with `rules: true` in its frontmatter, executes the companion `. | `--adr ` | Only check rules from a specific ADR | | `--verbose` | Show passing rules and timing info | +### Arguments + +| Argument | Description | +| ------------ | --------------------------------------------------------------------------------------------------------------- | +| `[files...]` | Optional file paths to scope checks to. Only ADRs whose `files` patterns match will run. Supports stdin piping. | + ### Exit codes -| Code | Meaning | -| ---- | --------------------------------------------- | -| 0 | All rules pass. No violations found. | -| 1 | One or more violations detected. | -| 2 | Internal error (e.g., malformed ADR or rule). | +| Code | Meaning | +| ---- | -------------------------------------------------------------------- | +| 0 | All rules pass. No violations found. | +| 1 | One or more violations detected. | +| 2 | Rule execution error (e.g., malformed rule, security scanner block). | ### Examples @@ -3186,6 +3192,18 @@ Check a single ADR: archgate check --adr ARCH-001 ``` +Check specific files (only matching ADRs run): + +```bash +archgate check src/foo.ts src/bar.ts +``` + +Pipe from git (check only changed files): + +```bash +git diff --name-only | archgate check --json +``` + Get JSON output for CI integration: ```bash @@ -3225,6 +3243,9 @@ When `--json` is used, the output is a single JSON object: { "message": "Command file must export a register*Command function", "file": "src/commands/broken.ts", + "line": 1, + "endLine": 1, + "endColumn": 42, "severity": "error" } ], @@ -3235,6 +3256,22 @@ When `--json` is used, the output is a single JSON object: } ``` +#### Violation fields + +| Field | Type | Description | +| ----------- | ------- | ------------------------------------------------------ | +| `message` | string | What the violation is | +| `file` | string? | Relative file path | +| `line` | number? | Start line (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 (guidance only) | +| `severity` | string | `"error"`, `"warning"`, or `"info"` | + +#### Blocked rule files + +When a rule file is blocked by the security scanner (e.g., uses `Bun.spawn()`) or a companion `.rules.ts` file is missing, the result appears in the JSON output with `status: "error"` and `ruleId: "security-scan"`. Violations include the exact file and line of the blocked code (or the `rules: true` line in the ADR for missing companions). + --- ## archgate adr create @@ -3718,16 +3755,22 @@ 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 | Line number in the file | -| `fix` | `string` | No | Suggested fix or remediation action | +| 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. --- @@ -3778,20 +3821,24 @@ interface ViolationDetail { 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?` | Line number in the file | -| `fix` | `string?` | Suggested fix | -| `severity` | `Severity` | Effective severity of this violation | +| 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 | ---