Deterministic German tenancy-law letter checks for rent increases and terminations
Find a file
Florian Minnecker 563be7389f
Some checks failed
CI / coverage (push) Successful in 50s
CI / test (push) Successful in 52s
CI / fuzz-build (push) Failing after 13s
CI / deny (push) Successful in 21s
feat: publish standalone legal-rules crate
2026-11-07 23:02:09 +01:00
.forgejo/workflows feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
.github/workflows feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
fuzz feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
src feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
tests feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
.gitignore feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
Cargo.toml feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
CHANGELOG.md feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
deny.toml feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
LICENSE-APACHE feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
LICENSE-MIT feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
README.md feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00
rust-toolchain.toml feat: publish standalone legal-rules crate 2026-11-07 23:02:09 +01:00

legal-rules

Deterministic German tenancy-law checks for landlord-to-tenant letters.

This crate is a pure rules engine. It has no HTTP client, database, AWS SDK, clock, filesystem access, or runtime state. Given the same typed input, it returns the same decision.

It covers the existing repr behavior for:

  • § 558 BGB rent-increase gates: residential use, agreed rent, twelve-month freeze, 15% / 36-month Kappungsgrenze, consent/effective-date timing, and German-language confirmation.
  • § 573 / § 573c BGB termination gates: stated legal ground, proof-capable delivery, notice-period steps, month-end effectiveness, arrears evidence, prior warning, and German-language confirmation.
  • Structural validation of a finished draft: addressee, sender, statutory citation, language, rent amount, and effective date.

This is not legal advice and does not inspect facts. Callers remain responsible for supplying accurate rent history, dates, warnings, and arrears evidence.

Install

The package is publishable but not yet on crates.io. Until the first release, use a path dependency from this checkout:

[dependencies]
legal-rules = { path = "../legal-rules" }

After a tagged release, use a pinned registry dependency:

[dependencies]
legal-rules = "0.1"

The library name is legal_rules.

The standalone repository is florian/legal-rules. The legal-rules/ directory in repr is the source of truth until the first registry release; the standalone repository is a subtree export of this directory, not a second implementation.

Quickstart

use legal_rules::{
    check_letter, ActionType, LetterContext, LetterLanguage, SendChannel,
};
use time::{Date, Month};

let context = LetterContext {
    is_commercial: false,
    agreed_rent: 1_000.0,
    proposed_rent: 1_090.0,
    notice_date: Date::from_calendar_date(2026, Month::January, 10)?,
    proposed_effective_date: Date::from_calendar_date(2026, Month::April, 1)?,
    historic_rents: vec![(
        Date::from_calendar_date(2024, Month::January, 1)?,
        1_000.0,
    )],
    tenancy_start: Some(Date::from_calendar_date(2020, Month::January, 1)?),
    legal_ground: None,
    arrears: None,
    prior_written_warning: false,
    send_channel: SendChannel::SimpleLetter,
    letter_language: Some(LetterLanguage::German),
};

let decision = check_letter(ActionType::DraftRentIncrease, context);
assert!(decision.allowed);
assert!(decision.requires_human_approval);
# Ok::<(), time::error::ComponentRange>(())

allowed == false is a hard block. requires_human_approval remains true for allowed rent-increase and termination letters; reminders and non-letter actions remain automatic.

API contract

API Purpose
check_letter(action, context) Decide whether an action may draft or send a letter.
validate_letter_draft(draft) Validate required fields, citations, language, and dates.
detect_german(text) Conservative, fail-closed German-language heuristic.
SendChannel::is_einwurfsicher() Whether a channel provides proof of receipt.

All input and result types are re-exported at crate root. Error results are a Vec<String> of human-readable problems; there is no hidden state or I/O error. The crate deliberately avoids a narrower builder API: callers construct the documented plain structs directly, including in compatibility tests.

Feature matrix

Feature Default Effect
none n/a The crate has no optional features or cloud/runtime dependencies.

The only runtime dependency is time, used for date types and civil date comparisons.

Scope boundaries

This package carries over the deterministic behavior of the original backend/aws/crates/legal-rules package. It intentionally does not add:

  • municipal rent-control ordinances or the separate § 558m cap, which need reviewed jurisdiction-specific inputs and legal review;
  • fact finding, PDF rendering, delivery, persistence, authentication, or AI;
  • a claim that a letter is legally sufficient in a particular case.

Unknown future inputs must extend the typed public API with a reviewed contract rather than being guessed from free text.

MSRV

Rust 1.89. The crate uses edition 2021 and is tested on stable Rust.

Semver policy

The crate follows semantic versioning for the public typed API. Breaking changes to public types, functions, enum variants, or documented decision semantics require a major release after 1.0, and a minor release while on 0.x. Additive fields on public structs are breaking because callers construct them, so they follow the same rule.

Testing

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo test --doc
cargo doc --no-deps
cargo deny check
cargo llvm-cov --all-features --summary-only --fail-under-lines 95
cargo check --manifest-path fuzz/Cargo.toml

The test suite includes old-caller compatibility tests, date and cap boundaries, empty and oversized inputs, property tests, and fuzz targets for both validate_letter_draft and detect_german. Run the fuzzers with cargo +nightly fuzz run validate_letter_draft and cargo +nightly fuzz run detect_german.

Release

Publishing requires a human with a crates.io token. Do not add publish credentials to the repository.

  1. Export this directory to the standalone florian/legal-rules repository (the directory is already a self-contained crate root).
  2. Run every gate in Testing in a clean checkout.
  3. Set CARGO_REGISTRY_TOKEN as a repository secret. It must belong to the floffel crates.io publisher and must never be committed.
  4. Run cargo publish --dry-run, then cargo publish from a tagged release commit.
  5. Switch consumers from a path dependency to a pinned registry version after the release is visible.

The local package is ready for that workflow; creating the external repository, tagging, and publishing are intentionally left to an authorized maintainer.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.