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 intent 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.
Path Alias Resolution
Section titled “Path Alias Resolution”Projects that use TypeScript path aliases (@app/*), Webpack aliases, or Docusaurus @site
get automatic alias resolution after every iw index build. CARI reads
compilerOptions.paths from tsconfig.json and tsconfig.base.json (following extends
chains) and rewrites aliased import specifiers in the index before any rule checks run.
Without this, a rule like no-cross-package-imports would see @site/src/foo as an
unresolved external import and raise false positives.
For aliases not defined in tsconfig.json, add them to .iw/config.yaml:
aliases: "@site": "microsite" # Docusaurus — maps @site/… → microsite/… "@app": "packages/app/src"Manual entries win on conflict with auto-detected aliases.
MCP Integration
Section titled “MCP Integration”intent_check and cari_rules_check are available as Copilot tools:
| MCP Tool | Purpose |
|---|---|
intent_check | Check all domains (structural / behavioral / documentary) |
cari_rules_check | Structural domain only (direct) |
@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