Lesson 7 of 7 · 9 min read · Enforcement

Attestation

The first six lessons covered what to declare. This one covers what happens when you do. Attestation is the surface where ArchRails turns a CALM architecture into a verdict on a proposed change — allowed, refused, or review required. Every refusal is reproducible from the inputs (the change, the architecture, the controls in force) and points at a specific node or relationship so the reviewer (or the agent) can act on it.

In this lesson
  1. What attestation is — and isn't
  2. The three verdicts
  3. Reading a finding
  4. The two surfaces — same engine
  5. Authoring for engagement
  6. Worked example · PII in a log line
  7. Deterministic by design
  8. Common mistakes

What attestation is — and isn't

At code-gen-time and at PR-time, ArchRails decides whether a proposed change respects the architecture you declared in your .calm.json and the controls attached to it. That decision is an attestation: a structured, signed record of what was checked, what the inputs were, and what the verdict was.

Attestation is the verdict, not the explanation. An AI may add prose to a finding so a reviewer can read it like English. But the verdict — pass / fail / review — comes from the deterministic rule engine. Models don't decide outcomes; they describe them.

What that buys you, concretely:

The three verdicts

Every attestation resolves to one of three outcomes. The vocabulary is deliberately small so reviewers / agents / dashboards all render the same thing.

allowed          The change does not violate any in-force control,
                 pattern, or flow constraint. The agent proceeds.
                 The PR check passes.

refused          The change violates one or more declared controls.
                 The agent sees the rule that fired and the
                 evidence that triggered it. The PR check fails;
                 a reviewer sees the same evidence as a comment.

review_required  The change touches a part of the architecture
                 ArchRails doesn't yet have a rule for, OR
                 introduces a network call to an unknown
                 destination. The agent pauses and asks the
                 human; the PR comment surfaces the gap.

review_required is the one most teams underestimate. The first time it fires on a real PR, the right move is usually to author a control or add a mapping rule that names the new surface — not to dismiss it. The engine errs on the side of "I need a human" when the architecture isn't explicit, not on the side of "looks fine."

Reading a finding

Every finding inside a refused or review_required verdict has the same shape. Reading it correctly the first time saves twenty minutes of debugging.

{
  "rule_id":        "AR-CTRL-SENSITIVE-LOG-001",
  "severity":       "error",
  "node_id":        "orders-service",
  "path":           "services/orders-service/src/persist.py",
  "summary":        "PII field 'tax_id' written to a logger call.",
  "evidence_excerpt": "logger.info(f\"tax_id={trade.tax_id}\")",
  "fix_guidance":   "Logs are not the audit trail. Use the orders-audit edge instead."
}

The two surfaces — same engine

ArchRails runs the attestation engine in two places. The vocabulary is identical; only the trigger differs.

Code-gen-time (MCP)
  Trigger:   An AI coding agent (Claude Code, Cursor, Windsurf,
             autonomous agent) is about to write code.
  Input:     The proposed change + the architecture in force.
  Verdict:   The agent receives `allowed` / `refused` / `review_required`
             before writing. A refusal means the wrong code never
             gets written to disk.

PR-time (CI)
  Trigger:   A pull request is opened / updated.
  Input:     The unified diff + the architecture in force.
  Verdict:   The PR check turns green or red; a structured review
             comment lists every finding with rule_id + evidence.

A change refused at code-gen-time will be refused at PR-time too. The inverse is also true: a change the agent generated cleanly will pass PR-time unless the architecture itself changed in the interim. This is the property that makes "the same engine runs in both places" valuable — you can trust the early verdict without rerunning it later.

Why two surfaces and not one? Catching at PR-time means a human or an agent has already written code that has to be reverted. Catching at code-gen-time means the wrong code never exists. Most teams want both: the early gate for productivity, the late gate for any change that didn't pass through the agent (a hotfix typed by hand, a refactor merged from another branch, etc.).

Authoring for engagement

A control only fires if its declared constraints engage with the change. "Engages" means the keys the constraint names appear in the change's evidence: a referenced module, a literal value, a property in your config, an identifier in the diff.

Pragmatically: when you author a control, name keys that will appear in the kind of change you want to catch.

// A control that catches PII written to logs.
// Engages when a logger call appears in the diff AND the
// logged expression references a PII-classified field.
"no-pii-in-logs": {
  "description": "PII fields must not be written to logs.",
  "requirements": [
    {
      "control-requirement-url": "https://controls.myorg.io/no-pii-in-logs.schema.json",
      "control-config-url":      "https://controls.myorg.io/orders-service/no-pii-in-logs.config.json"
    }
  ]
}

// The config the schema validates against:
{
  "forbidden_in_logs": ["tax_id", "ssn", "card_number", "date_of_birth"],
  "log_call_patterns":  ["logger.info", "logger.warning", "console.log"]
}

The control engages when the diff adds a line matching one of log_call_patterns and the line references one of forbidden_in_logs. A change to an unrelated file in the same service doesn't engage this rule, so it doesn't fire (and doesn't get counted in attestation noise).

The engagement model is what keeps attestation precise as your architecture grows. A node with twenty controls doesn't trigger twenty findings on every PR — it triggers findings for the controls whose keys actually appear in the change.

Worked example · PII in a log line

Here's the full loop: agent proposes a change, attestation refuses, agent revises. Same control as above.

1. Agent proposal (Claude Code, inline edit on persist.py)

   + logger.info(f"tax_id={trade.tax_id}")

2. MCP attestation runs against the proposed change

   rule_id:          AR-CTRL-SENSITIVE-LOG-001
   node_id:          orders-service
   evidence_excerpt: logger.info(f"tax_id={trade.tax_id}")
   summary:          PII field 'tax_id' written to a logger call.
   fix_guidance:     Logs are not the audit trail. Use the
                     orders-audit edge instead.
   decision:         refused

3. Agent revises in-context

   - logger.info(f"tax_id={trade.tax_id}")
   + audit.record({"trade_id": trade.id, "actor": user.id})

4. MCP attestation runs again on the revised change

   findings: []
   decision: allowed

5. Agent writes the file

The wrong code never reached disk. The right code that did reach disk carries an audit record — the orders-audit edge satisfies the same compliance requirement the log line was trying to satisfy, but through the architecturally-correct path.

Deterministic by design

Attestation verdicts are deterministic. That word is doing real work — here's what it means in practice:

Why this matters for regulated teams. Generic AI code review bots optimize for plausible suggestions drawn from open-source training data. ArchRails enforces your architecture, with the rule that fired traceable to a node ID in your repo. When the regulator asks "what was enforced on this merge?", you can answer with literal rule IDs and literal evidence, not "the model said it was OK."

Common mistakes

I authored a control but it never fires.

The constraint's keys aren't appearing in the changes you expect to catch. Open the dashboard, find a PR you expected to trigger the control, and check the evidence the engine extracted. If the keys you named in control-config-url aren't in the evidence, your rule won't engage. Either rename the keys to match what your code actually does, or broaden the constraint's matching pattern.

Every PR gets review_required — that's a lot of human review.

Usually one of two things. Either the changed paths aren't covered by mapping rules in .archrails/config.yml (so they resolve to "unknown node, ask for review"), or the architecture lacks declared relationships for the network calls in the diff. Add the mapping entries and the missing connects edges; the noise drops fast.

An agent sees "refused" but writes the code anyway.

The verdict is advisory to the agent's planner — the file system isn't hooked. If an agent ignores refusals consistently, the failsafe is the PR-time gate: the same engine will refuse the merge. The agent doesn't have the keys to bypass that. If you want stricter behavior at code-gen- time, revoke the agent's API key — without a key, the MCP server returns 401 and the agent's MCP client errors out before any write.

The model "explained" a verdict in a way that contradicts the rule.

That's a presentation bug, not a verdict bug. The rule that fired is the authority — the prose is downstream. File a bug with the request_id from the dashboard's record; the engine's verdict in the audit log will show what actually happened, separate from how it was narrated.

Can I write a control that fires only on Friday afternoons?

No. Attestation is a function of the diff and the architecture, not of wall-clock time. If you want time-windowed gating, layer it on top: have your CI step check the day-of-week before invoking ArchRails. ArchRails itself stays purely deterministic on (diff, architecture, controls).