Semantic Rule Checking
import { Aside, Code, Tabs, TabItem } from ‘@astrojs/starlight/components’;
What Is a Semantic Rule?
Section titled “What Is a Semantic Rule?”CARI’s layers-check and boundary-violations commands detect violations in the import graph — which module imports which. Semantic rules go one level deeper: they detect violations in the AST of each file — what a module does inside its own code.
Import-graph violations (layers-check): File A ──imports──► File B ← edge in the import graph
Semantic violations (rules-check): File A ──accesses──► item.resource.path ← property access chain File A ──calls─────► parseRef() ← function invocation File A ──defines───► itemsById Map ← forbidden symbol pattern File A ──imports───► packages/data/** ← import path patternThis matters because import boundaries can be clean while the code still violates architectural intent. A UI component that accesses item.resource.path and applies domain parsing logic may have legal imports — but it’s doing resolver work that belongs in the adapter layer.
Quick Start
Section titled “Quick Start”# Extract rules from an ADRiw intent extract docs/ADR-003.md --provider openai --output .iw/rules.yaml
# Check all three domainsiw intent checkiw intent check --domain structural # import + AST rules onlyiw intent check --domain behavioral # Mermaid sequence/flow rulesiw intent check --domain documentary # coverage + stale docs + terminologyiw intent check --domain all # explicit all-domains pass
# CI: only changed files, only high severityiw intent check --changed src/auth.ts --severity high --format json
# Regression gating: fail only if violations increasediw intent check --baseline .iw/baseline.jsonThe rules.yaml File
Section titled “The rules.yaml File”Rules live in .iw/rules.yaml (or the path given to --config):
version: 1
# Optional: explicit allowed layer flows for the prescriptive diagram (§17.2)allowed: - from_layer: apps/ui to_layer: apps/api description: "UI may call the API gateway"
rules: - id: no-internal-field-access-in-ui description: "UI components must not access internal resource fields directly" adr: ADR-001 severity: high forbidden: - type: property_access chain: "**.resource.path" in: "apps/ui/src/components/**"Top-Level allowed: Block
Section titled “Top-Level allowed: Block”When present, the allowed: list declares which layer-to-layer flows are explicitly sanctioned.
This powers the green arrows in the Prescriptive Architecture Diagram.
When absent, the diagram derives permitted flows from layer order automatically.
| Field | Required | Description |
|---|---|---|
from_layer | yes | Source layer name (must match a layer in .iw/layers.yaml or inferred) |
to_layer | yes | Destination layer name |
description | no | Rationale shown in the CLI and diagram hover panel |
Rule Types
Section titled “Rule Types”Each rule has one or more forbidden clauses. Every clause has a type:
import_pattern — Forbidden Import Paths
Section titled “import_pattern — Forbidden Import Paths”Flags any import whose resolved path matches the given glob.
- id: no-ui-to-db severity: high forbidden: - type: import_pattern pattern: "packages/data/**" # glob matched against the imported file path in: "apps/ui/**" # only check these filesThis is the only type that works without full AST extraction — it uses the existing import graph. Use it when the ADR says “layer X must never import layer Y”.
| Field | Description |
|---|---|
pattern | Glob matched against the imported file path |
in | Restrict to files matching this glob |
except | Whitelist files that are allowed to break the rule |
target_layer | Only flag if the imported file belongs to this named layer (prevents false positives when path patterns overlap across packages) |
property_access — Forbidden Property Chains
Section titled “property_access — Forbidden Property Chains”Flags any access to a property chain matching the pattern (e.g. item.resource.path).
- id: no-internal-field-access-in-ui severity: high forbidden: - type: property_access chain: "**.resource.path" # glob-style chain matcher; ** matches any prefix in: "apps/ui/src/**" except: "apps/ui/src/adapters/**"Chains are extracted from the AST: member_expression nodes deeper than 2 levels are
recorded in the property_accesses table with their full chain.
| Field | Description |
|---|---|
chain | Glob pattern matched against the full property chain (e.g. **.source.path) |
in | Restrict to files matching this glob |
except | Whitelist files or directories |
except_pattern | Whitelist accesses whose enclosing symbol name matches this regex |
taint_propagation | true to track variables assigned from this chain (§16.1) |
call — Forbidden Function Calls
Section titled “call — Forbidden Function Calls”Flags any call to a function whose name matches the pattern.
- id: no-parse-path-in-ui severity: high forbidden: - type: call callee: "parseCategory|parsePath|normalizeRef" # pipe-separated names or regex in: "apps/ui/src/**"Combine with context_access to only flag calls that co-occur with a forbidden property access:
- id: no-regex-on-source-metadata severity: high forbidden: - type: call callee: "match|exec" in: "apps/**" context_access: "**.resource.path" # only flag when .resource.path is accessed nearby| Field | Description |
|---|---|
callee | Pipe-separated name list or regex |
in | Restrict to files matching this glob |
except | Whitelist files |
context_access | Only flag when co-located with an access chain matching this pattern |
taint_propagation | true to track calls on variables assigned from a tainted source (§16.1) |
symbol_name — Forbidden Symbol Declarations
Section titled “symbol_name — Forbidden Symbol Declarations”Flags any symbol (variable, constant, function, class) whose name matches the pattern.
- id: no-entity-map-in-views severity: medium forbidden: - type: symbol_name pattern: "itemsById" # exact name or regex in: "apps/ui/src/components/**"Use scope to restrict to only exported or top-level symbols:
- id: no-exported-helpers-in-views severity: low forbidden: - type: symbol_name pattern: ".*Helper$" in: "apps/ui/src/**" scope: exported # exported | top-level | any (default: any)| Field | Description |
|---|---|
pattern | Name pattern (exact or regex) |
in | Restrict to files matching this glob |
scope | exported, top-level, or any (default) |
cypher — Custom Graph Queries (CypherLite)
Section titled “cypher — Custom Graph Queries (CypherLite)”Run a custom Cypher query against the CARI SQLite graph. Returns one row per violation.
The query must return columns file, line, and detail.
- id: no-untested-public-api description: "Exported functions in packages/ must have a corresponding test" severity: medium forbidden: - type: cypher query: > MATCH (s:Symbol {kind: 'function', exported: true}) WHERE s.file STARTS WITH 'packages/' AND NOT EXISTS { MATCH (t:Symbol) WHERE t.file CONTAINS '/test/' AND t.name CONTAINS s.name } RETURN s.file AS file, s.line AS line, s.name AS detailCypherLite supports a subset of Cypher syntax directly against the SQLite schema — no Neo4j required. Use this for custom cross-table checks that the built-in types cannot express.
variable_assignment — Forbidden Assignment Patterns
Section titled “variable_assignment — Forbidden Assignment Patterns”Flags variable assignments where the right-hand side matches a pattern.
- id: no-hardcoded-base-url severity: medium forbidden: - type: variable_assignment rhs_pattern: "https?://[^/]+\\.(prod|staging)\\.example\\.com" in: "src/**" except: "src/config/**"property_chain_length — Overly Deep Property Access
Section titled “property_chain_length — Overly Deep Property Access”Flags any property chain access exceeding a maximum depth.
- id: no-deep-access-in-ui description: "Warn when UI accesses deeply nested properties (often a layering smell)" severity: low forbidden: - type: property_chain_length max_length: 4 # flag chains with more than 4 segments in: "apps/ui/src/components/**"Rule Options
Section titled “Rule Options”taint_propagation — Track Values Through Variable Assignments
Section titled “taint_propagation — Track Values Through Variable Assignments”When a forbidden property_access or call result is stored in a local variable, the violation can silently escape detection if only the original expression is checked. Enable taint_propagation: true to have CARI follow intra-function def-use chains and flag any subsequent access or call through tainted variables.
- id: no-internal-field-access-in-ui severity: high forbidden: - type: property_access chain: "**.resource.path" in: "apps/ui/src/**" taint_propagation: true # also flag uses of vars assigned from .resource.pathWithout taint propagation, this code is not flagged:
const path = item.resource.path; // direct access — flagged ✓const label = path.split("/").pop(); // indirect use — NOT flagged without taint_propagationWith taint_propagation: true, the assignment const path = item.resource.path taints the
variable path, and any subsequent property_access or call on path within the same function
is also reported as a violation.
FAIL no-internal-field-access-in-ui [high] apps/ui/src/components/ItemCard.tsx:4 item.resource.path (direct) apps/ui/src/components/ItemCard.tsx:5 path.split (taint: path ← item.resource.path)Taint tracking is intra-function only — it does not follow values across function boundaries or
module exports. For inter-procedural analysis, combine with a call rule that targets the function
that consumes the tainted value.
| Field | Type | Default | Description |
|---|---|---|---|
taint_propagation | boolean | false | Follow intra-function def-use chains from the forbidden expression |
count_mode — Deduplication
Section titled “count_mode — Deduplication”By default, each occurrence in a file counts as a separate violation. Use count_mode: per_file
to report at most one violation per file:
- id: no-any-cast-in-services severity: low count_mode: per_file # at most one violation per file (not per occurrence) forbidden: - type: symbol_name pattern: "as any" in: "packages/services/**"autofix — Suggested Fixes
Section titled “autofix — Suggested Fixes”Attach a human-readable fix hint that appears in rules-check output and JSON reports:
- id: no-source-path-in-ui severity: high autofix: suggestion: "Use entity.properties.displayPath instead of entity.source.path" docs_url: "https://example.com/adr/ADR-003#resolver-layer" forbidden: - type: property_access chain: "**.source.path" in: "apps/ui/**"expresses — Visualization Intent
Section titled “expresses — Visualization Intent”A rule may carry an expresses block to define named components and their flows for the
Prescriptive Architecture Diagram. These are visualization-only
rules — they produce no CI violations.
- id: adr003-provider-adapter-flow description: "Intended ADR-003 pipeline: Providers → Adapters → Pipeline Workers" adr: ADR-003 severity: low expresses: elements: - name: SourceProvider kind: component # component | interface | service | store layer: "packages/providers" - name: AdapterParser kind: component layer: "packages/adapters" - name: PipelineWorker kind: component layer: "packages/pipeline" flows: - from: SourceProvider to: AdapterParser policy: allowed kind: data - from: AdapterParser to: PipelineWorker policy: allowed kind: control forbidden: [] # explicit empty — no CI enforcementCombine expresses with a real forbidden clause to both visualize intent and enforce it:
- id: no-ui-to-db expresses: elements: - name: UILayer kind: component layer: "apps/ui" - name: DatabaseLayer kind: component layer: "packages/data" flows: - from: UILayer to: DatabaseLayer policy: forbidden forbidden: - type: import_pattern pattern: "packages/data/**" in: "apps/ui/**"Checking Rules
Section titled “Checking Rules”# Check all rules, all filesiw index rules-check
# ASCII conformance diagram (default) — omit with --no-diagramiw index rules-check --no-diagram
# CI: only changed files, only high severity, JSON outputiw index rules-check --changed src/auth.ts,src/data/repo.ts \ --severity high --format json
# Check a single rule by IDiw index rules-check --rule-id no-source-path-in-ui
# Use a custom config pathiw index rules-check --config infra/rules.yamlCLI Output — ASCII Conformance Diagram
Section titled “CLI Output — ASCII Conformance Diagram”By default rules-check prints an ASCII conformance diagram summarising the layer topology:
Architecture Conformance
[OK] apps/ui ──✓──▶ apps/api UI may call the API gateway [OK] apps/api ──✓──▶ packages/services API may use domain services [HIGH] apps/ui ─────▶ packages/data no-ui-to-db: 3 violation(s) ● no-service-names-in-ui [MED]
15/17 rules clean · 3 total violation(s)Suppress the diagram with --no-diagram for minimal CI output.
CLI Options
Section titled “CLI Options”| Flag | Default | Description |
|---|---|---|
--config <path> | .iw/rules.yaml | Path to rules.yaml |
--changed <files> | (all files) | Comma-separated changed file list for incremental CI |
--severity <level> | low | Minimum severity: high, medium, or low |
--rule-id <id> | (all rules) | Check only a specific rule |
--limit <n> | 100 | Maximum violations to report |
--format <fmt> | text | Output format: text or json |
--no-diagram | (off) | Suppress the ASCII conformance diagram |
--db <path> | (auto) | Path to index.db |
Extracting Rules from ADRs
Section titled “Extracting Rules from ADRs”Use rules-extract to have an LLM read your ADRs and draft a rules.yaml:
# Basic extraction — forbidden rules onlyiw index rules-extract docs/ADR-003.md --provider openai --output .iw/rules.yaml
# Also synthesize allowed: entries from ADR prose (§17.3)iw index rules-extract docs/ADR-003.md docs/ADR-005.md \ --provider openai \ --output .iw/rules.yaml \ --with-allowed
# Also extract architectural layer hintsiw index rules-extract docs/ADR-003.md \ --provider openai \ --output .iw/rules.yaml \ --with-allowed \ --with-layer-hints \ --layers-output .iw/layers.hints.yaml
# Append newly found rules without overwriting existing onesiw index rules-extract docs/ADR-006.md --provider openai \ --output .iw/rules.yaml --appendrules-extract Options
Section titled “rules-extract Options”| Flag | Default | Description |
|---|---|---|
--provider <name> | openai | LLM provider: openai or smart-mock |
--model <name> | gpt-4o | Model name |
--api-key <key> | $OPENAI_API_KEY | API key override |
--output <path> | (stdout) | Output path for the rules.yaml draft |
--append | (off) | Append new rules, skipping duplicate IDs |
--with-allowed | (off) | Also extract explicit allowed: permission entries |
--with-layer-hints | (off) | Also extract architectural layer hints |
--layers-output <path> | .iw/layers.hints.yaml | Output path for layer hints YAML |
-v, --verbose | (off) | Show prompt and response details |
The extracted YAML is a draft — always review before committing. The LLM only emits rules for constraints that are explicitly stated in the ADR.
Using in CI
Section titled “Using in CI”- name: Check architectural rules run: | iw index rules-check \ --changed "${{ steps.changed.outputs.files }}" \ --severity high \ --format json > violations.json jq -e '.violations | length == 0' violations.jsonSee GitHub Actions / CI for a full workflow example.
MCP Integration
Section titled “MCP Integration”rules-check is available as a Copilot tool:
| MCP Tool | Purpose |
|---|---|
cari_rules_check | Check rules, optionally for changed files only |
@workspace Does this change violate any ADRs?Example: A Complete rules.yaml
Section titled “Example: A Complete rules.yaml”version: 1
allowed: - from_layer: apps/ui to_layer: apps/api description: "UI may call the API gateway" - from_layer: apps/api to_layer: packages/services description: "API handlers use domain service interfaces" - from_layer: packages/services to_layer: packages/data description: "Services access repositories"
rules: - id: no-ui-to-db description: "UI must never import the data layer directly" adr: ADR-003 severity: high forbidden: - type: import_pattern pattern: "packages/data/**" in: "apps/ui/**"
- id: no-source-path-in-ui description: "UI components must not access entity.source.path" adr: ADR-003 severity: high autofix: suggestion: "Use entity.properties.displayPath instead" forbidden: - type: property_access chain: "**.source.path" in: "apps/ui/src/components/**"
- id: no-ref-resolution-in-ui description: "UI components must not resolve $ref strings" adr: ADR-003 severity: medium forbidden: - type: call callee: "refToId|idToName" in: "apps/ui/src/components/**" - type: property_access chain: "**.$ref" in: "apps/ui/src/components/**" except: "apps/ui/src/components/resolver/**"
- id: no-entitybyid-in-views description: "entityById maps must not be built in view components" adr: ADR-003 severity: medium forbidden: - type: symbol_name pattern: "entityById" in: "apps/ui/src/components/views/**"
- id: adr003-pipeline-flow description: "Intended pipeline: Providers → Adapters → Workers (viz only)" adr: ADR-003 severity: low expresses: elements: - { name: SourceProvider, kind: component, layer: "packages/providers" } - { name: AdapterParser, kind: component, layer: "packages/adapters" } - { name: PipelineWorker, kind: component, layer: "packages/pipeline" } flows: - { from: SourceProvider, to: AdapterParser, policy: allowed, kind: data } - { from: AdapterParser, to: PipelineWorker, policy: allowed, kind: control } forbidden: []Next Steps
Section titled “Next Steps”- Prescriptive Architecture Diagram — visualize your rules as a layered SVG architecture report
- Architecture Analysis — layer inference, boundary checks, and the full architecture report
- Ensure Intent in Code — CI integration patterns
- CLI Reference — full command documentation