Skip to main content

Generic policy validator for the RM Method — init/check/explain over any repo, driven entirely by rm-policy.yaml.

Project description

rm-validate

PyPI CI License

A generic policy validator for the RM Method. It reads a repo's own rm-policy.yaml, runs the checks that its declared capabilities gate plus a non-negotiable universal base, and reports file · rule · value vs. limit · severity with an exit code driven by the policy's enforcement mode.

The validator is generic by design: it assumes nothing about the layout of a target repo. Every rule, path, threshold and layer pattern comes from that repo's rm-policy.yaml. Where a check needs to know "what is what" in the code (e.g. which folder is the domain layer), that mapping is declared explicitly in the policy — the validator never guesses, and never silently skips.

Disclaimer: rm-validate is tooling for the RM Method, provided as-is, without warranties. It runs cheap, high-value checks; it is not a full policy engine, type checker, or security scanner. Licensed under Apache-2.0.


Install

# recommended — isolated global CLI
pipx install rm-validate

# or plain pip
pip install rm-validate

# or from source (before the first PyPI release, or to track main)
pipx install "git+https://github.com/edarm92-arch/rm-tooling"

Only runtime dependency: PyYAML. Python 3.11+.

Quickstart

# 1. Infer capabilities from the code and write a commented policy (run once).
rm-validate init .

# 2. Confirm the suggested profile with an ADR (e.g. ADR/0001-perfil-app.md).

# 3. Validate — locally and in CI.
rm-validate check .

# Trace any check: what activates it, what parametrizes it, which languages.
rm-validate explain engines_must_be_pure .
  • init — infers capabilities from the real code (with evidence), suggests a profile (static / app / platform), and writes an evidence-commented rm-policy.yaml. It configures; it does not validate.
  • check — runs the universal base + capability-gated + enabled fitness checks. Exit code follows enforcement.mode.
  • explain <check> [path] — traceability: activation, parametrizing policy key, required layering_patterns keys, supported languages, and (given a path) files analyzed vs. matched.

If a repo has no rm-policy.yaml, check does not fail or grade it — it degrades gracefully (exit 0) and points you to init. The validator is an advisor until rules are declared, then a judge.

Capabilities are the truth; profiles are shortcuts

A rule applies if and only if the system has the thing that rule protects. Capabilities are declared in the policy and re-inferred on every check; profiles are just presets of capability defaults (platform extends app extends static), overridable per capability.

Capability Gated checks
(universal base — always on) secrets_scan, changelog, dependency_audit_hook, file_limits, prompt_gate
has_database db_migrations_present
fitness (opt-in via fitness_functions) no_circular_dependencies, domain_must_not_import_infrastructure, ui_must_not_access_db_directly, engines_must_be_pure, mutations_server_side_only
config-driven forbidden_deps (when forbidden_imports is declared)

The universal base is not toggleable. Adding a new check means mapping it to a capability or the universal base — a check that maps to no existing capability proposes a new capability in review, never invents one silently.

Layering patterns (declared, never inferred)

Layer-based fitness functions need to know which globs are which layer. That mapping is always declared by hand in layering_patterns — inferring folder conventions is a high-risk silent false negative, so the method forbids it. If a layering fitness function is enabled without the keys it needs, check fails with config incompleta: falta layering_patterns.<key> — even in warning mode (it is config integrity, not code severity).

fitness_function Requires in layering_patterns
domain_must_not_import_infrastructure domain, infrastructure
ui_must_not_access_db_directly ui, db_access
engines_must_be_pure engines
mutations_server_side_only ui (or server_mutations)
no_circular_dependencies none — import-graph, layer-agnostic

Multi-language support

Import-based checks route through language analyzers (Ports & Adapters). Supporting a new language means adding an adapter under rm_validate/analyzers/ — the checks never change.

The invariant: a check that cannot analyze the files its globs matched never reports success. "Could not analyze" is indistinguishable from "no violations" in an exit 0, and that indistinguishability is the false confidence. So an unsupported language fails as config incompleta (even in warning mode). A check that "does not apply" must be set false in the policy — an honest human act — never resolved to green by emptiness.

Check Languages (v0.1.0) Mechanism
engines_must_be_pure Python ast (stdlib, precise)
domain_must_not_import_infrastructure Python · TS/JS ast / regex
ui_must_not_access_db_directly Python · TS/JS ast / regex
mutations_server_side_only Python · TS/JS ast / regex
no_circular_dependencies Python only ast + graph

Cycle detection needs a precise, complete graph; TypeScript module resolution (path aliases, barrels, tsconfig.paths) is genuinely hard, so TS graph support is deferred (v0.2.0), not faked. Pointing no_circular_dependencies at TS globs fails honestly. The ImportAnalyzer port allows future adapters to delegate to mature tools (madge, dependency-cruiser, import-linter) without breaking the "only external dep: PyYAML" rule; if an external analyzer is unavailable the check fails honestly rather than pretending.

The four anti-theatre locks

All four are config-integrity failures — they block even in warning mode (only severity violations are downgraded to warnings), and they are not optional:

  1. Profile ADRcheck fails if require_profile_adr is on and no ADR matches adr_glob. The lazy shortcut of declaring a profile without an owned decision does not pass.
  2. Layering config — an enabled layering fitness function without its declared globs fails (see above).
  3. Capability mismatch — capabilities are re-inferred every run. Asymmetric: under-declaring (has_database: false with migrations/ present) fails (it silences a rule that should apply); over-declaring (exposes_mcp: true with no MCP code) is only a warning (it just adds the repo's own overhead).
  4. CI self-checkcheck is a required CI step, not discipline. rm-validate runs check . on itself in its own CI.

Baseline + ratchet (legacy adoption)

Enable enforcement.baseline_ratchet to adopt the validator on an existing codebase. The first run inventories current violations into .rm/baseline.json as accepted debt; later runs surface (and, in blocking mode, fail on) only new violations. A resolved violation leaves the baseline — the file only ever decreases. Config-integrity findings are never baselined.

Enforcement modes

  • warning (default) — reports everything, exit 0 for severity findings. Start here.
  • blocking — a severity (error) finding fails the merge (exit 1). The Director flips this from the policy once things stabilize.

Config-integrity findings (the four locks, unsupported languages) fail in both modes.

rm-policy.yaml reference (essentials)

extends: app                    # static | app | platform (preset of capabilities)
capabilities:                   # per-capability overrides (the truth)
  has_database: true
enforcement:
  mode: warning                 # warning | blocking
  require_profile_adr: true
  adr_glob: "ADR/*perfil*.md"
  capability_mismatch_fails: true
  baseline_ratchet: { enabled: false, baseline_file: ".rm/baseline.json", block_only_new_violations: true }
modularity:
  code_file_lines: { soft: 300, hard: 500 }
  context_file_lines: { hard: 500 }
fitness_functions:
  no_circular_dependencies: true
layering_patterns:              # declared globs, never inferred
  domain: ["app/domain/**"]
  infrastructure: ["app/adapters/**"]
graph_scope: ["**/*.py"]        # scope for no_circular_dependencies (Python only)
prompt_gate:
  required_sections: [objetivo, criterios_de_aceptacion, rollback]
  paths: ["prompts/**/*.md"]
security:
  no_secrets_in_repo: true
  dependency_scan_in_ci: true
forbidden_imports:              # optional generic import-edge bans
  - name: "adapters must not import domain internals"
    src: ["app/adapters/**"]
    dst: ["app/domain/_internal/**"]

Capability inference cannot be excluded from a policy. There is no inference: key — an inference-exclude switch would let a repo silence the capability-mismatch lock from the very file the lock must police. Fix a false positive by improving the detection pattern upstream in rm-tooling (a PR that benefits everyone), never by a local switch. A total-repo exclusion glob (**, *, .) in any exclusion list is a config error, and any hand-declared modularity.exclude_globs entry is surfaced as a WARNING inference_exclusion (structural excludes like node_modules stay silent). A judge the accused can switch off is not a judge.

Full, annotated examples for each profile live in examples/ (static, app, platform).

Extending: add a language

  1. Add an adapter class in rm_validate/analyzers/ implementing ImportAnalyzer (LANGUAGES, supports_graph, extract_imports, prepare, resolve).
  2. Register it in rm_validate/analyzers/registry.py.

No check changes. If your adapter cannot back a precise graph, set supports_graph = False — the coverage gate will keep it out of cycle detection honestly.

Self-adoption

rm-validate applies the RM Method to itself: it ships its own rm-policy.yaml (profile static, no capabilities) and ADR/0001-perfil-static.md. Its CI runs rm-validate check . for real — if any lock regressed on this repo, its own CI would go red, exactly like any consumer.

Development

pip install -e ".[dev]"
ruff check rm_validate tests
mypy rm_validate
pytest -q
python -m rm_validate check .   # self-check

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rm_validate-0.1.1.tar.gz (54.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rm_validate-0.1.1-py3-none-any.whl (60.3 kB view details)

Uploaded Python 3

File details

Details for the file rm_validate-0.1.1.tar.gz.

File metadata

  • Download URL: rm_validate-0.1.1.tar.gz
  • Upload date:
  • Size: 54.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rm_validate-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e6b79eb4f8c457f7eb37fc8a49456541ffd7bfd3796076bed7e1307ded43f191
MD5 e78050a0f00b86fe148c2544c7c71313
BLAKE2b-256 33bec20a3437b0b77aef44ff2147b1f7c3e58631f5c5f828b6ac02ac8596fefa

See more details on using hashes here.

Provenance

The following attestation bundles were made for rm_validate-0.1.1.tar.gz:

Publisher: release.yml on edarm92-arch/rm-tooling

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rm_validate-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: rm_validate-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 60.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rm_validate-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 bf6a9372d6343ad0771e8a338e2b02a3ade731e2371bb806c40ea4e2b9df3eda
MD5 3bf070c86e09f89af917c88dec9f3cf3
BLAKE2b-256 2ec9842361c04df89bc1ae2efb931ef7674944f329a15d42a068dd815df27f43

See more details on using hashes here.

Provenance

The following attestation bundles were made for rm_validate-0.1.1-py3-none-any.whl:

Publisher: release.yml on edarm92-arch/rm-tooling

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page