How-to guide

Turn an ADR into an enforceable decision

Add a ## Constraints section to an architecture decision record (ADR) with a FORBID_LITERAL rule: the exact text it forbids and the paths it covers. Import the ADR, and mneme check fails any file under those paths that contains the text. The example is ADR-012: request handlers never import the database client.

You’ll end up with: an ADR that makes mneme check fail when a file under app/handlers/ imports the database client, plus a JSON verdict that records why.

Goal

An ADR (architecture decision record) explains a decision to the people who read it. A coding agent or a CI job never reads it. Mneme compiles the ADR into a decision: the ADR's text plus a typed rule naming the exact text it forbids and the paths where the ban applies.

Mneme stores compiled decisions in one JSON file, .mneme/project_memory.json. This guide calls it the memory file.

Prerequisites

Command
pipx install "mneme-hq==0.10.0"
  • A repository with a docs/adr/ directory. The example talks about app/handlers/ (request handlers), app/repositories/ (data access) and app/db/client.py (the database client). Those paths don't need to exist: mneme check reads the file you pass and treats --target-path as the path that content would have.
  • No coding agent. The next guide adds one.

Configure

1. Create the memory file

Run this from the repository root. It creates an empty memory file and refuses to overwrite an existing one.

Command
mneme init
Recorded output
Created .mneme/project_memory.json

2. Write the ADR with a Constraints section

Save this as docs/adr/ADR-012-handlers-use-repositories.md. Mneme reads the YAML frontmatter and the ## Constraints section. It keeps the prose in between as the decision's rationale.

File · docs/adr/ADR-012-handlers-use-repositories.md
---
id: ADR-012
title: Request handlers do not import the database client
status: accepted
priority: foundational
date: "2026-10-01"
scope: handlers
---
Handlers call repositories. A direct client import skips the transaction
and tenancy handling that lives in app/repositories.

## Constraints
- FORBID_LITERAL:
    value: from app.db.client import
    include_paths:
      - "app/handlers/**"
  • status: accepted puts the ADR in the active set, the decisions Mneme checks. The import leaves proposed, deprecated and superseded ADRs out.
  • Changing an ADR's status later does not remove its decision from the memory file. To retire a decision, write a new ADR that lists the old one under supersedes, keep the old file in docs/adr, and re-import with --update-existing (see Troubleshooting). Don’t edit the memory file by hand: from 0.10.0 a hand-edited file can fail to load.
  • priority is required. When two ADRs share a scope, the higher priority wins.
  • scope: handlers names the area the ADR governs, and lookup matches on it (see Give coding agents the decisions that apply to a task). Give every accepted ADR its own scope. When two share one, the import keeps only one of them; see Troubleshooting.
  • FORBID_LITERAL is the rule. Its value is exact, case-sensitive text.
  • include_paths limits the rule to files under app/handlers/. Without it, the rule applies to every file.

The ADR import reference lists every frontmatter field and directive.

3. Preview the import

Without --apply, the import writes nothing. Check that ADR-012 is in the active set and the rule reads the way you meant it.

Command
mneme adr import docs/adr --memory .mneme/project_memory.json
Recorded output
ADR import preview
============================================================

Active set (1 ADRs):
  [ADR-012] status=active
      rule: FORBID_LITERAL from app.db.client import
        include_paths: app/handlers/**

4. Apply it

Command
mneme adr import docs/adr --memory .mneme/project_memory.json --apply
Recorded output
Wrote 1 decisions to .mneme/project_memory.json

Run

Here is a handler that breaks ADR-012. Save it anywhere, for example inputs/invoices_direct.py:

File · inputs/invoices_direct.py
from app.db.client import get_connection


def list_invoices(customer_id: str):
    conn = get_connection()
    return conn.execute("SELECT * FROM invoices WHERE customer_id = ?", (customer_id,))

Check it the way an agent hook (a check that runs inside a coding agent before it writes a file) or a CI job would. --input is the file to read. --target-path is the path that content will have in the repository, which is what the rule's include_paths are matched against. --query describes the change; Mneme uses it to choose which decisions to show as guidance, and checks typed rules such as this one either way.

Command
mneme check --memory .mneme/project_memory.json \
  --input inputs/invoices_direct.py \
  --target-path app/handlers/invoices.py \
  --query "edit to app/handlers/invoices.py"

Expected result

The check fails, names ADR-012, and exits with code 2:

Recorded output
FAIL  [ADR-012] FORBID_LITERAL "from app.db.client import" -- trigger: from app.db.client import
      Request handlers do not import the database client
      path: app/handlers/invoices.py via app/handlers/**

PATH  APPLIED  [ADR-012] app/handlers/invoices.py via app/handlers/** -- an include selector matched

Result: FAIL

Run the same content against a repository path. ADR-012 doesn't apply there, so the check passes with exit code 0:

Command
mneme check --memory .mneme/project_memory.json \
  --input inputs/invoices_direct.py \
  --target-path app/repositories/invoices.py \
  --query "edit to app/repositories/invoices.py"
Recorded output
PATH  EXCLUDED [ADR-012] app/repositories/invoices.py -- no include selector matched

Result: PASS

A handler that calls the repository instead passes too. Save it as inputs/invoices_repo.py:

File · inputs/invoices_repo.py
from app.repositories.invoices import InvoiceRepository


def list_invoices(customer_id: str):
    return InvoiceRepository().for_customer(customer_id)
Command
mneme check --memory .mneme/project_memory.json \
  --input inputs/invoices_repo.py \
  --target-path app/handlers/invoices.py \
  --query "edit to app/handlers/invoices.py"
Recorded output
PATH  APPLIED  [ADR-012] app/handlers/invoices.py via app/handlers/** -- an include selector matched

Result: PASS

Evidence

Add --json to get a verdict a machine can read. Keep it as a CI artifact or attach it to the pull request.

Command
mneme check --memory .mneme/project_memory.json \
  --input inputs/invoices_direct.py \
  --target-path app/handlers/invoices.py \
  --query "edit to app/handlers/invoices.py" \
  --json > verdict.json
python -m json.tool verdict.json
Recorded output
{
    "schema": "mneme.check/v1",
    "verdict": "FAIL",
    "mode": "strict",
    "evaluation_complete": true,
    "applicability": [
        {
            "decision_id": "ADR-012",
            "rule_type": "FORBID_LITERAL",
            "rule_value": "from app.db.client import",
            "rule_index": 0,
            "path_scoped": true,
            "input_path": "app/handlers/invoices.py",
            "outcome": "APPLIED",
            "selector": "app/handlers/**",
            "reason": "an include selector matched"
        }
    ],
    "violations": [
        {
            "decision_id": "ADR-012",
            "decision_text": "Request handlers do not import the database client",
            "severity": "FAIL",
            "rule": "from app.db.client import",
            "trigger": "from app.db.client import",
            "kind": "typed_rule",
            "rule_type": "FORBID_LITERAL",
            "input_path": "app/handlers/invoices.py",
            "selector": "app/handlers/**"
        }
    ],
    "freshness": []
}
  • verdict is PASS, WARN, FAIL or INCOMPLETE (the check could not finish).
  • mode is strict by default: FAIL and INCOMPLETE exit 2 and WARN exits 1. Pass --mode warn to report WARN and FAIL without failing; INCOMPLETE still exits 2.
  • evaluation_complete must be true before you trust the verdict. An incomplete evaluation exits 2 and never reports PASS.
  • applicability shows which path selector matched, so you can see why a rule applied or didn't.

The audit sorts every decision in the memory file into four tiers: Protected, Mneme-ready, Requires modelling and Guidance (Install Mneme and activate your first protection explains them). ADR-012 carries a rule that mneme check enforces, so the audit counts it as Protected:

Command
mneme audit --memory .mneme/project_memory.json --repo-root .
Recorded output
Architecture Protection Audit
============================================================
Decisions discovered:         1
Protection-relevant:          1

Current Protection:           100.0%
Protection Gap:               0.0%

Protected today:              1
Mneme-ready:                  0
Requires modelling:           0
Guidance-only:                0

Per-decision breakdown:
  [protected] ADR-012: Request handlers do not import the database client
      guardrail: FORBID_LITERAL: from app.db.client import

Troubleshooting

The check passes on a file that contains the import

You left out --target-path. Mneme then matches the rule against the input file's own path, inputs/invoices_direct.py, which isn't under app/handlers/:

Recorded output
PATH  EXCLUDED [ADR-012] inputs/invoices_direct.py -- no include selector matched

Result: PASS

Pass the repository path the content belongs to.

An ADR is missing from the active set

Two accepted ADRs share a scope. The import keeps the one with the higher priority, or the newer date, and drops the other without a warning. Here ADR-013 also uses scope: handlers:

File · troubleshooting/same-scope/ADR-013-handlers-no-http.md
---
id: ADR-013
title: Request handlers do not call HTTP services directly
status: accepted
priority: foundational
date: "2026-10-02"
scope: handlers
---
Handlers call service clients in app/clients.

## Constraints
- FORBID_LITERAL:
    value: import requests
    include_paths:
      - "app/handlers/**"

Import both, and ADR-012 disappears from the active set:

Recorded output
Active set (1 ADRs):
  [ADR-013] status=active
      rule: FORBID_LITERAL import requests
        include_paths: app/handlers/**

Give each ADR its own scope. A dotted scope such as handlers.http keeps them apart, and both stay active. Dotted scopes change how the Model Context Protocol (MCP) lookup finds an ADR; Give coding agents the decisions that apply to a task explains.

Recorded output
Active set (2 ADRs):
  [ADR-012] status=active
      rule: FORBID_LITERAL from app.db.client import
        include_paths: app/handlers/**
  [ADR-013] status=active
      rule: FORBID_LITERAL import requests
        include_paths: app/handlers/**

The import stops with missing YAML frontmatter

The import crashes with a Python traceback and exit code 1. The last line names the file and the problem:

Recorded output
mneme.adr_schema.ADRParseError: troubleshooting/nygard/ADR-012.md: missing YAML frontmatter (expected file to start with '---')

The ADR uses a Nygard-style **Status:** Accepted line instead of YAML frontmatter:

File · troubleshooting/nygard/ADR-012.md
# ADR-012: Request handlers do not import the database client

**Status:** Accepted

Handlers call repositories.

Mneme doesn't parse those. Add the frontmatter block from step 2 to the top of the file.

The import stops with unknown constraint directive

Same traceback, different last line:

Recorded output
mneme.adr_constraints.ConstraintParseError: unknown constraint directive 'FORBID_LITERALS' (expected one of ['FORBID_DEPENDENCY', 'FORBID_LITERAL', 'FORBID_PATH', 'REQUIRE_PATH'])

A directive name has a typo. Mneme stops the import instead of skipping the rule, so a misspelling can't switch enforcement off without anyone noticing.

ADR import refused: id 'ADR-012' already exists

Recorded output
ERROR: ADR import refused: id 'ADR-012' already exists in canonical decision authority. Pass --update-existing to create or reuse an immutable version occurrence, or rename the incoming ADR.

You applied the ADR before. After you edit it, re-run the import with --update-existing, which records the edit as a new version of the decision and keeps the earlier one:

Command
mneme adr import docs/adr --memory .mneme/project_memory.json --apply --update-existing

On memory migrated from Mneme 0.9.x, the import also refuses to drop a rule that the migration carried over, so changing or removing one needs --preserve-protection or --release-protection for each rule it names. See adr import.

Limits

  • The rule matches exact text. import app.db.client as db is different text, so this handler passes:
    File · inputs/invoices_alias.py
    import app.db.client as db
    
    
    def list_invoices(customer_id: str):
        return db.get_connection().execute("SELECT * FROM invoices")
    Recorded output
    PATH  APPLIED  [ADR-012] app/handlers/invoices.py via app/handlers/** -- an include selector matched
    
    Result: PASS
    Add one FORBID_LITERAL per import form you need to block, or run an import linter in CI alongside Mneme.
  • Only FORBID_LITERAL gives a FAIL that respects include_paths. A one-word FORBID_DEPENDENCY value, like requests, gives a WARN wherever the word appears, whatever the path. A name made of several words, like left-pad, is checked only when lookup picks that decision for the query. In strict mode a WARN exits 1, and the Claude Code hook blocks on it. Here is ADR-014, which bans requests:
    File · troubleshooting/dependency/ADR-014-no-requests.md
    ---
    id: ADR-014
    title: Services use the shared HTTP client
    status: accepted
    priority: foundational
    date: "2026-10-03"
    scope: network
    ---
    Use app.clients.http so retries and timeouts stay consistent.
    
    ## Constraints
    - FORBID_DEPENDENCY: requests
    Recorded output
    WARN  [ADR-014] constraint "no requests" -- trigger: requests
          Services use the shared HTTP client
    
    Result: WARN
    FORBID_PATH and REQUIRE_PATH are stored for guidance and the audit, and don't change a verdict.
  • This guide runs checks by hand. Stop an AI coding agent from crossing an architectural boundary runs the same check before an agent writes a file, and Fail a pull request that breaks an ADR runs it on every pull request.

Next