@archrad/deterministic

MCP serverDev tools

Checks infrastructure definitions against lint rules and policies, detecting configuration drift before deployment.

Unavailable. This server has no hosted endpoint yet, so Ahel can't serve it.

Add to setup to save this item as a reference. Ahel cannot run it, and signing in will not install it.

About this server

Deterministic IR/IR-LINT validation, policy packs, drift vs exports (archrad). Apache-2.0.

Getting started

  1. Save this item in Your setup as a reference.
  2. Read the source or reference documentation for its setup requirements. Saving it here does not connect it to your AI.
  3. Check this page for availability before trying to install it through Ahel.

From the project's README

As published by archradhq/arch-deterministic in README.md.

Your architecture drifts before you write a single line of code. archrad validate catches it — deterministically, in CI, before the PR merges.

Define your system as a graph. ArchRAD compiles it, lints it against architecture rules, and tells you exactly what's wrong — with rule codes, not opinions.

New in 0.7.x — archrad demo runs a full example with zero setup, and archrad scan drafts an IR from any repo with every node cited to file:line. Lint findings only decrease in this release; nothing that passed on 0.6.x can start failing. See the changelog.


Quick start (60 seconds)

npm install -g @archrad/deterministic
archrad demo

No IR file, no flags, no config — runs from any directory. Lints a bundled example and shows you the findings:

    orders-api (http)  ──▶  orders-db (postgres)

⚠️ IR-LINT-DIRECT-DB-ACCESS-002: API node "orders-api" connects directly to datastore node "orders-db"
   Fix: Introduce a service or domain layer between HTTP handlers and persistence.
⚠️ IR-LINT-MISSING-AUTH-010: HTTP entry node "orders-api" has no auth node or auth config …
⚠️ IR-LINT-NO-HEALTHCHECK-003: No HTTP node exposes a typical health/readiness path …

Then run it on your own repo

archrad scan . --out draft.ir.json    # draft an IR — every node cited to file:line
archrad validate --ir draft.ir.json   # lint it

scan reads your Docker Compose, Kubernetes manifests, Terraform, OpenAPI, package manifests, and source code, then grades every node by confidence so you can see what was parsed versus guessed. The output is a normal IR file — review it, edit it, commit it.

Already have an OpenAPI spec or a Backstage catalog? Skip the scan:

archrad ingest openapi --spec ./openapi.yaml --out ./graph.json
archrad ingest backstage --catalog . --out ./graph.json

Once a graph is committed, archrad validate is your CI gate — exit 1 blocks the merge.


What it does

ArchRAD is a blueprint compiler and governance layer. You define your architecture as an IR — nodes, edges, allowed connections — and ArchRAD validates it against a deterministic rule engine. The same IR, the same rules, the same inputs always produce the same findings.

CommandWhat it checksCodes
archrad validateGraph structure + architecture lintIR-STRUCT-* IR-LINT-*
archrad lintArchitecture lint only (fast inner-loop; skips structural)IR-LINT-*
archrad explain <code>Canonical rule guidance without running a pass—
archrad policies-sha256 --dir <policies>Generate a archrad-policy-pack.sha256 manifest for signed PolicyPacks—
archrad validate-driftIR vs generated code on diskDRIFT-*
archrad scanDraft IR from a repo — topology, OpenAPI, manifests, code; every node cited + graded by confidence—
archrad reconstructDraft IR from source code alone (one of scan's four signal sources, usable standalone)—
archrad ingest openapiDerive IR from OpenAPI (local path or https URL for --spec; -H for URL auth headers)—
archrad ingest backstageBackstage catalog-info.yaml → IR (Component, Resource, API, System; Location file targets)—
archrad fragment mergeMerge 2+ IR files — union by node.id (conflicts → stderr); --prefix-fragments for disjoint union—
archrad exportCompile IR → FastAPI or Express + Docker—

Ingest + merge workflows: docs/INGEST.md. All commands / flags: docs/CLI_REFERENCE.md. Codegen (export): docs/EXPORT.md.


Draft an IR from a real repo (archrad scan)

archrad scan points at a repository and emits a draft IR — never a final answer, always something to review and edit. It runs six extractors, graded by how much they have to guess:

SourceSignalConfidence
Topologydocker-compose.yml, Kubernetes manifestshigh — a declaration, parsed for real
InterfaceOpenAPI / Swaggermedium — documents a real surface, but only what's documented
Infrastructure-as-codeTerraform (*.tf)medium — a real declaration, but read via regex, not a true HCL parse
Manifestpackage.json, requirements.txt, go.mod, pom.xmllow — a driver dependency implies an edge, not proof it's used
Codepattern scan of source (Node.js/TS, Python, C#)low — regex over text, no semantic understanding

Every node and edge carries config.provenance[] — inferred_from: "file:line", a confidence, and which extractor found it — so nothing has to be taken on faith. When two extractors describe the same thing, scan merges them (keeping the highest-confidence body, unioning all provenance) instead of duplicating or erroring.

archrad scan . --dry-run                       # print the draft, write nothing
archrad scan ./server --out draft.ir.json       # write it
archrad scan . --extractors compose,manifest    # only run specific extractors
archrad scan . --scope all                      # include tests, examples, docs, and demos

CLI scans default to --scope production so fixtures, documentation examples, Storybook stories, and test-only manifests do not become production findings. Use --scope all when those artifacts are the subject of the review. Library callers retain the backwards-compatible scope: 'all' default.

The output is a normal IR file — pipe it straight into archrad validate once you've reviewed it. Full flags: docs/CLI_REFERENCE.md.


Implementation governance (archrad reconstruct + --codebase)

"The IR looks clean — but is that what was actually shipped?"

The validation pipeline above checks your authored IR (the design contract). The --codebase flag bridges the gap to the real codebase by reconstructing an IR from source code and comparing them.

The "dummy IR" attack — and how to catch it

A developer can author a compliant IR (passes all IR-STRUCT-* and IR-LINT-* rules) while the actual code bypasses the documented architecture. The most dangerous pattern: an IR that shows a clean service layer but code that directly queries the database.

# Catch discrepancies between the authored design and the shipped code
archrad validate --ir authored-ir.json --codebase ./src --report findings.html

When --codebase is provided, the pipeline runs three stages:

  1. IR-STRUCT-* structural validation on the authored IR
  2. IR-LINT-* architecture lint on the authored IR
  3. IR-DRIFT-IMPL-* comparison of authored IR vs reconstructed codebase IR

IR-DRIFT-IMPL-* rules

Implementation drift uses a separate exit threshold: --fail-on, --fail-on-warning, and --max-warnings apply only to IR-STRUCT-, IR-LINT-, and merged PolicyPack findings. IR-DRIFT-IMPL-* are gated solely by --impl-drift-fail-on (default: drift severities error fail the command).

CodeSeverityWhat it catches
IR-DRIFT-IMPL-000warningAuthored IR could not be parsed for drift comparison (fix structural issues first)
IR-DRIFT-IMPL-001warningIR declares HTTP-like entry nodes but reconstruction detected zero artifacts in --codebase
IR-DRIFT-IMPL-002warningHTTP / health routes in code but authored IR has no HTTP-like nodes
IR-DRIFT-IMPL-003errorDirect DB connection in code, no DB edge in authored IR
IR-DRIFT-IMPL-004errorHTTP route in code not present in authored IR
IR-DRIFT-IMPL-005warningService-to-service call in code, no edge in authored IR
IR-DRIFT-IMPL-006infoAuth middleware in code, no auth node in authored IR

IR-DRIFT-IMPL-003 is the critical one. An error there means the authored IR hides a direct DB dependency.

Reconstruct standalone

# Just reconstruct — write the IR without comparing
archrad reconstruct --from ./src --output reconstructed-ir.json

# Force language detection
archrad reconstruct --from ./src --language python --output py-ir.json

# Print to stdout (dry-run)
archrad reconstruct --from ./src --dry-run

# Show every detected artifact
archrad reconstruct --from ./src --verbose

Supported languages

LanguageDetected patterns
Node.js / TypeScriptExpress, Fastify, NestJS routes + controllers; BullMQ workers, Agenda jobs, cron schedules; pg, Prisma, TypeORM, Sequelize, Mongoose, Redis, BullMQ, Firebase; Passport, express-jwt, NestJS guards, Auth0, Okta, Keycloak, Cognito; axios, got, node-fetch, gRPC; outbound HTTP URLs extracted per destination
PythonFlask, FastAPI, Django URLs, DRF @action; SQLAlchemy, psycopg2, asyncpg, PyMongo, motor, redis-py; login_required, jwt_required, FastAPI OAuth2; requests, httpx, aiohttp, gRPC
C#Minimal API MapGet/MapPost; ASP.NET Core [HttpGet]/[ApiController]; EF Core DbContext, Npgsql, Dapper; [Authorize], AddAuthentication, JWT bearer; HttpClient, gRPC, RestSharp

Service decomposition (Node.js)

The reconstructor detects service boundaries and creates one node per service rather than collapsing everything into a single gateway:

LayoutDetection signalResult
Files in routes/ or controllers/ directoryrouter.get/post/… in dedicated fileOne service node per file
NestJS controllers@Controller decorator / *.controller.ts namingOne service node per controller file
Entry file with app.listen()Mounts other routersgateway node
BullMQ / Agenda / cron filesnew Worker(…), agenda.define(…)worker node
Monolithic file (all routes in one file)Single file, no routes/ dirSingle gateway node — no forced split

Edges between nodes reflect what the reconstruction found:

  • gateway → service edges are added for all decomposed services.
  • service → database edges appear only when the route file itself contains a DB import or env var reference (not when DB access is hidden behind a shared utility module).

Node naming

DB and cache nodes use the most user-recognizable name available:

  1. Env var name — process.env.REDIS_URL → node named "redis", process.env.SESSION_REDIS_URL → "session-redis"
  2. Connection variable name — const userDb = new Pool(…) → node named "userDb" (for unique variable names)
  3. Library / driver name — fallback when no env var or named variable is detectable

Node IDs and names never include env_var or other detection-method metadata.

External service identification

Outbound HTTP/gRPC calls create distinct external nodes per destination:

  • axios.post('https://api.stripe.com/…') → node "stripe"
  • fetch('https://auth0.com/oauth/token') → node "auth0"
  • new PaymentServiceClient('payments-svc:50051') → node "payment" (gRPC)
  • Generic fallback for clients without a detectable URL: one shared "external-service" node

Honest scope limits

Reconstruction is best-effort signal, not certainty:

  1. Dynamic patterns — eval, reflection, metaprogramming, and generated code are not detected.
  2. Runtime config — topology decisions made at runtime (feature flags, config-driven routing) cannot be statically analyzed.
  3. Cross-language services — a single codebase containing multiple language runtimes reduces accuracy.
  4. Heavy abstractions — macro-based or annotation-processor-heavy frameworks may obscure routes or connections.
  5. Shared data access layers — when DB connections live in a shared utility file (e.g., db.ts, firebaseAdmin.ts) rather than the route files themselves, those connections are not linked to individual service nodes. The reconstructed IR honestly omits edges it cannot trace statically.

Interpreting lint findings on reconstructed IR

Run archrad validate on a reconstructed IR and you may see:

FindingOn reconstructed IRInterpretation
IR-LINT-DEAD-NODE-011 (service node, no outgoing edges)Expected when services use a shared data-access layerNot a real problem; the route files have no directly-detected downstream edges
IR-LINT-HIGH-FANOUT-004 on gatewayExpected for large monoreposThe gateway→service edge count reflects the route file count
IR-LINT-DIRECT-DB-ACCESS-002 on gatewayReal findingA route in the entry file directly accesses a DB without a service layer
IR-LINT-NO-HEALTHCHECK-003May fire if health endpoint is in a separate routes/health.ts service nodeCheck whether the health route is reachable from the entry point

IR-LINT-DIRECT-DB-ACCESS-002 on reconstructed IR is the high-signal rule: if it fires on a gateway node, it means the entry file itself has direct DB access (not delegated to a service layer). If the decomposition correctly identified service nodes between the gateway and the database, this rule should be silent.

Treat reconstructed IR as a draft for human review, not as ground truth. Use it to catch obvious drift between authored architecture and actual code, not as an authoritative picture of the system.

Treat IR-DRIFT-IMPL-* findings as "review required", not absolute truth. The reconstructed IR is signal, not certainty.

Worked example — catching a "dummy IR"

Authored IR (authored.json) shows clean layered architecture:

{ "graph": { "nodes": [
  { "id": "api", "type": "gateway", "name": "API" },
  { "id": "svc", "type": "service", "name": "Order Service" },
  { "id": "db", "type": "postgres", "name": "Orders DB" }
], "edges": [
  { "from": "api", "to": "svc" },
  { "from": "svc", "to": "db" }
]}}

But src/api/routes.ts contains:

import { Pool } from 'pg';
const db = new Pool({ connectionString: process.env.DATABASE_URL });
// direct DB query in the route handler — bypasses the service layer
app.get('/orders', async (req, res) => res.json(await db.query('SELECT * FROM orders')));

Running archrad validate --ir authored.json --codebase ./src:

❌ IR-DRIFT-IMPL-003: Direct database connection(s) detected in code (pg (PostgreSQL) → postgres) but no DB edges exist in the authored IR
   Fix: Either add the missing DB edges to the authored IR, or confirm the codebase points to the correct service.
   Suggestion: This discrepancy is a governance red flag: the authored IR could be masking a direct DB dependency.
   Impact: CRITICAL — this pattern is exploited in "dummy IR" attacks where clean design docs conceal direct database access in shipped code.

The IR said no direct DB access. The code said otherwise. IR-DRIFT-IMPL-003 caught the gap.

Project config (archrad.yml)

Drop an archrad.yml at the root of your repo and skip re-typing flags:

# archrad.yml
ir: ./archrad-graph.json
target: python
output: ./generated
failOn: warning
policies: ./policies
archrad validate          # uses ir, failOn, policies
archrad export            # uses ir, target, output
archrad validate-drift    # uses ir, target, output

archrad walks upward from the CWD looking for archrad.yml (or archrad.yaml). Explicit CLI flags always override the config. Use --no-config to ignore any discovered file, or --config <path> to point at a non-standard location. Full schema: docs/CONFIG.md.

Fast inner loop: archrad lint + archrad explain

# Iterate on lint without re-checking IR structure every run:
archrad lint --ir ./graph.json

# Focus on a single rule while fixing it:
archrad lint --ir ./graph.json --rule IR-LINT-MISSING-AUTH-010

# Understand a rule code without running a pass:
archrad explain IR-LINT-DIRECT-DB-ACCESS-002
archrad explain --list                  # every known rule code

archrad lint is the fast inner loop; use archrad validate once before the CI gate to also enforce IR structural shape. With archrad.yml at repo root, both run with no flags.

CI integration

# Fail on any structural error (default):
archrad validate --ir ./graph.json

# Also fail on lint warnings:
archrad validate --ir ./graph.json --fail-on-warning

# Machine-readable output for GitHub Actions:
archrad validate --ir ./graph.json --json

MCP server (Cursor / Claude Desktop)

After install, archrad-mcp is on your PATH. Add it to your IDE:

{
  "mcpServers": {
    "archrad": { "command": "archrad-mcp" }
  }
}

Your agent can call the same engine as the CLI via six MCP tools (e.g. archrad_validate_ir, archrad_lint_summary, archrad_validate_drift, archrad_policy_packs_load, archrad_list_rule_codes, archrad_suggest_fix). See docs/MCP.md for parameters and local testing.


How it works (architecture)

IR (nodes/edges)  →  validateIrStructural (IR-STRUCT-*)  →  errors block export
                           ↓
                    validateIrLint (IR-LINT-*)  →  warnings (CI: --fail-on-warning / --max-warnings)
                           ↓
              pythonFastAPI | nodeExpress generators
                           ↓
              openapi.yaml + app code + package metadata
                           ↓
              golden layer (Dockerfile, docker-compose.yml, Makefile, README; host→container e.g. 8080:8080)
                           ↓
              validateOpenApiInBundleStructural(openapi.yaml)  →  document-shape warnings (not full API lint)
                           ↓
              { files, openApiStructuralWarnings, irStructuralFindings, irLintFindings }

  Optional CI: archrad validate-drift  →  re-export IR in-memory, diff vs existing ./out  →  DRIFT-MISSING / DRIFT-MODIFIED (thin deterministic gate)

Validation levels (quick contract)

  1. JSON Schema validation — IR document shape vs schemas/archrad-ir-graph-v1.schema.json (editor/CI; optional at runtime).
  2. IR structural validation — validateIrStructural: arrays, ids, HTTP config, edge refs, cycles (IR-STRUCT-*). Uses an internal normalized graph (see docs/IR_CONTRACT.md).
  3. Export-time generated OpenAPI structural validation — Parse + required fields on the generated openapi.yaml (document shape, not Spectral).

Architecture lint (IR-LINT-*) sits after structural checks: rule visitors on the parsed graph (heuristics, not schema).

Validation layers (naming)

Layer (OSS)What it isCodes
IR structural validationGraph well-formedness: ids, edges, cycles, HTTP path/methodIR-STRUCT-*
Architecture lint (basic)Deterministic heuristics only (no AI, no org policy)IR-LINT-*
OpenAPI structural validation (document shape)Parse + required top-level OpenAPI fields on generated spec(string warnings, not IR codes)
Layer (Cloud — not this package)Examples
Policy engineSOC2, org rules, entitlement
Architecture intelligenceDeeper NFR / cost / security reasoning
AI remediationRepair loops, suggested edits
  1. IR structural validation: duplicate/missing node ids, bad HTTP config.url / config.method, unknown edge endpoints, directed cycles.
  2. Architecture lint: Implemented as a registry of visitor functions on a parsed graph (buildParsedLintGraph → LINT_RULE_REGISTRY in src/lint-rules.ts). If the IR cannot be parsed, buildParsedLintGraph returns { findings } (IR-STRUCT-) instead of null; use isParsedLintGraph() or call validateIrLint, which forwards those findings. Each rule returns IrStructuralFinding[]; runArchitectureLinting / validateIrLint flatten them. Custom org rules: compose runArchitectureLinting with your own (g) => findings in CI (worked example: docs/CUSTOM_RULES.md), or fork and append to LINT_RULE_REGISTRY if the stock archrad validate CLI must emit your codes. CLI archrad validate / archrad export print lint under **Architecture lint (IR-LINT-)** (grouped separately from structural). Codes include IR-LINT-DIRECT-DB-ACCESS-002, IR-LINT-SYNC-CHAIN-001, IR-LINT-NO-HEALTHCHECK-003, IR-LINT-HIGH-FANOUT-004, IR-LINT-ISOLATED-NODE-005, IR-LINT-DUPLICATE-EDGE-006, IR-LINT-HTTP-MISSING-NAME-007, IR-LINT-DATASTORE-NO-INCOMING-008, IR-LINT-MULTIPLE-HTTP-ENTRIES-009, IR-LINT-MISSING-AUTH-010, IR-LINT-DEAD-NODE-011. Sync-chain depth counts synchronous edges only; mark message/queue/async hops via edge.metadata.protocol / config.async (see edgeRepresentsAsyncBoundary in lint-graph.ts and docs/ENGINEERING_NOTES.md).
  3. Generators → openapi.yaml, handlers, deps.
  4. Golden path → make run / docker compose up --build.
  5. OpenAPI document shape on the bundle — not Spectral-level lint. Issues → openApiStructuralWarnings.

IR contract: schemas/archrad-ir-graph-v1.schema.json. Parser boundary + normalized shapes: docs/IR_CONTRACT.md (normalizeIrGraph → materializeNormalizedGraph).

Trust builder: IR-STRUCT-* errors block export; IR-LINT-* warnings are visible and can gate CI via --fail-on-warning / --max-warnings; OpenAPI shape issues surface as export warnings.

Reference (OSS): docs/DRIFT.md (deterministic validate-drift), docs/RULE_CODES.md (finding codes; MCP docsUrl targets GitHub anchors), docs/MCP.md (MCP tools + local testing).

Codegen vs validation (retry, timeouts, policy)

Generators may emit retry/timeout/circuit-breaker code when the IR carries matching edge or node config (e.g. retryPolicy). That is code generation, not a guarantee. OSS does not currently require or lint “every external call must have timeout/retry” — that class of rule is semantic / policy and fits ArchRad Cloud or custom linters on top of the IR.


Ways to use it

ModeBest forExample
CLIQuick local scaffolding, CI, “no Node project” usagearchrad export --ir graph.json --target python --out ./out
YAML → IRAuthor graphs in YAML, emit JSON for validate/exportarchrad yaml-to-ir -y graph.yaml -o graph.json
OpenAPI → IRDerive HTTP nodes from OpenAPI 3.x (same IR shape as YAML path); ArchRad Cloud merge uses the same libraryarchrad ingest openapi --spec openapi.yaml -o graph.json
CLI validateCI / pre-commit: IR structural + architecture lint, no codegenarchrad validate --ir graph.json
CLI validate-driftAfter export or merges: on-disk tree vs fresh deterministic export from same IRarchrad validate-drift -i graph.json -t python -o ./out
Library (@archrad/deterministic)IDPs / pipelinesrunDeterministicExport → files + findings; runValidateDrift / runDriftCheckAgainstFiles for drift
MCP (archrad-mcp)Cursor / Claude Desktop / other MCP hostsstdio server: validate IR, lint summary, drift, policy packs, static archrad_suggest_fix — see docs/MCP.md

Shortened here. Read the whole README on GitHub.

Signals

GitHub stars
1
Forks
1
Last commit
Sep 2026
Weekly downloads
58
Weekly_downloads
53 weekly_downloads
Advanced
Delivery
deterministic MCP server → your Ahel connector (mcp.ahel.ai) → your AI.
Item type
mcp-server
Key
io-github-archradhq-deterministic
Source
github.com/archradhq/arch-deterministic