| Each firm's data is isolated by the database | Postgres row-level security, forced on every firm table; the app role never owns tables. | src/law2/schema/rls.py | test_rls_isolation.py, test_tenancy_end_to_end.py | Passed |
| A non-member learns nothing about a firm | Any firm route answers 404 to a non-member, never 403, so firms cannot be enumerated. | src/law2/api/deps.py | test_tenant_resolve.py | Passed |
| Signed-in changes are CSRF-protected | Signed-in requests that change data require a derived CSRF token, except the pre-sign-in forms (login, signup, password reset) and the email-provider webhook, which is verified by its own signature. A test walks every route. | src/law2/api/csrf.py | test_api_mutations.py | Passed |
| A repeated request does not act twice | Firm POSTs require an Idempotency-Key, checked before any work, except sign-in, sign-out, accepting an invitation, client signup and staff sign-off, which repeat safely by design. | src/law2/api/idempotency.py | test_api_idempotency.py | Passed |
| Sessions and invitations are stored hashed | A leaked backup is not a set of live sessions or invitations. | src/law2/auth/sessions.py, src/law2/auth/invitations.py | test_auth.py, test_email_tokens.py | Passed |
| Logins and lookups are rate-limited | Per IP and per account, with counters kept even when the request fails. | src/law2/api/ratelimit.py | test_ratelimit.py | Passed |
| Law4All contacts only permitted hosts | One allowlist module holds every permitted host; a code scan fails if any other module names one. | src/law2/acquire/allowlist.py | test_allowlist.py | Passed |
| A CAPTCHA is never solved | If a court page shows a CAPTCHA, the fetch stops; nothing is solved, retried or queued for a person. | src/law2/acquire/transport_browser.py, src/law2/acquire/case_details.py | test_transport_browser.py, test_case_details.py | Passed |
| The court's CAPTCHA-gated case-status page is not fetched | A person opens that page beside Law4All and pastes it; Law4All does not load, frame or proxy it. Separately, Law4All reads the court's case details only for cases on a published cause list it holds. | src/law2/web/static/app.js | test_get_from_court.py | Passed |
| A client sees only what the firm shares | Client views are built from an allow-list of fields; firm notes, time entries and unshared documents do not reach them. | src/law2/clients/projection.py | test_client_view.py | Passed |
| Facts drawn from a firm's documents stay with that firm | Risk lines drawn from a firm's documents appear only in that firm's plan, notes and reports, not in a client's view or another firm's plan. (Where the files themselves are stored is listed below.) | src/law2/strategy/doc_risks.py | test_doc_risks.py | Passed |
| Law4All never says 'not listed' for a day it does not hold | Next-hearing statements carry how far the held cause lists reach. | src/law2/api/next_scope.py | test_next_scope.py | Passed |
| A name match is never treated as the same person | Same-name links are shown as name matches with their words; nothing is merged. | src/law2/documents/party_links.py | test_party_links.py | Passed |
| Comparable-case figures carry their hand-checked precision | Matter-class comparables quote their measured precision from one table of hand-checked samples. | src/law2/api/accuracy.py | test_matter_class.py | Passed |
| Court text is never rendered as HTML | The interface builds pages with textContent only, under a strict content security policy. | src/law2/web/static/app.js | test_web_ui.py | Passed |
| Published taxonomy versions are frozen | Each published court-taxonomy version is a file pinned by its SHA-256; a test fails if one changes, and the build refuses to overwrite a published version. | src/law2/api/taxonomy.py, src/law2/lake/taxonomy/build.py | test_taxonomy_api.py, test_taxonomy_build.py | Passed |