# Guarantees &amp; capabilities ledger

Every guarantee Sandloom promises and every capability a user can rely on, with the tests, `sandloom check` rules and import contracts that prove it. The ledger (`guarantees.yml`) is a CI gate (`tests/architecture/test_guarantee_ledger.py`): an enforcing test that disappears, an entry deleted without a `removed:` record, or a new ADR section without an entry fails the build.

**429 guarantees, 174 capabilities, 37 recorded gaps.**

Run exactly the enforcing tests (from the repository root):

```
uv run pytest -m guarantees src
```

## Guarantees

### G-TENANT

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-TENANT-01 | An ORM read, update or delete against a TenantScoped model with no tenant bound (and not inside unscoped()) raises MissingTenantError instead of running cross-tenant. | `sandloom/core/tests/test_tenant_isolation.py::test_no_tenant_bound_fails_closed`<br>`sandloom/core/tests/test_tenant_criteria_prebuilt.py::test_no_tenant_fails_closed`<br>`sandloom/core/tests/test_tenant_guard_early_exit.py::test_tenant_statements_are_still_scoped_and_fail_closed` |
| G-TENANT-02 | Once a tenant is bound, ORM SELECT, bulk UPDATE and bulk DELETE on a TenantScoped model are automatically filtered to that tenant's rows only. | `sandloom/core/tests/test_tenant_isolation.py::test_automatic_filtering_to_bound_tenant`<br>`sandloom/core/tests/test_tenant_isolation.py::test_bulk_update_scoped_to_bound_tenant`<br>`sandloom/core/tests/test_tenant_isolation.py::test_bulk_delete_scoped_to_bound_tenant` |
| G-TENANT-03 | The ORM guard also covers relationship loads (lazy, selectin, joined), joins, EXISTS/IN subqueries, select\_from and Session.get (an identity-map hit is re-checked against the bound tenant): unbound they fail closed, bound they return only the bound tenant's rows. | `sandloom/core/tests/test_tenant_guard_gaps.py::test_unbound_reach_into_tenant_table_fails_closed`<br>`sandloom/core/tests/test_tenant_guard_gaps.py::test_unbound_lazy_relationship_load_fails_closed`<br>`sandloom/core/tests/test_tenant_guard_gaps.py::test_bound_relationship_loads_are_scoped`<br>`sandloom/core/tests/test_tenant_guard_gaps.py::test_session_get_identity_map_hit_is_rechecked` |
| G-TENANT-04 | An ORM object insert of a TenantScoped model inherits the bound tenant, fails with MissingTenantError when none is bound, and fails with TenantMismatchError when it names a different tenant\_id. | `sandloom/core/tests/test_tenant_isolation.py::test_insert_inherits_bound_tenant`<br>`sandloom/core/tests/test_tenant_isolation.py::test_insert_without_tenant_fails_closed`<br>`sandloom/core/tests/test_tenant_isolation.py::test_insert_cannot_target_another_tenant` |
| G-TENANT-05 | Under a bound tenant, bulk INSERTs, bulk UPDATEs that change tenant\_id (values or ordered\_values) and bulk-by-primary-key UPDATEs on a TenantScoped model raise UnsafeTenantOperationError; they are only allowed inside explicit unscoped(). | `sandloom/core/tests/test_tenant_isolation.py::test_bulk_insert_requires_explicit_system_scope`<br>`sandloom/core/tests/test_tenant_isolation.py::test_bulk_update_cannot_change_tenant_id`<br>`sandloom/core/tests/test_tenant_guard_gaps.py::test_bulk_update_by_primary_key_is_refused`<br>`sandloom/core/tests/test_tenant_guard_gaps.py::test_ordered_values_cannot_change_tenant_id` |
| G-TENANT-06 | Core select/insert/update/delete statements (including a raw Table alias mixed into an ORM query) against a recognised TenantScoped table under a bound tenant are refused with UnsafeTenantOperationError when they cannot be safely scoped. | `sandloom/core/tests/test_infra_review_20261022.py::test_core_tenant_table_statements_cannot_escape_bound_scope`<br>`sandloom/core/tests/test_infra_review_20261022.py::test_mixed_orm_and_core_alias_cannot_read_another_tenant` |
| G-TENANT-07 | The explicit helpers scoped(), tenant\_filter() and current\_tenant\_id() raise MissingTenantError when no tenant is bound, and scoped() restricts results to the bound tenant. | `sandloom/core/tests/test_tenant_isolation.py::test_explicit_scoped_is_fail_closed`<br>`sandloom/core/tests/test_tenant_isolation.py::test_single_tenant_runtime_scope_binds_canonical_tenant`<br>`sandloom/core/tests/test_ledger_runtime_pins.py::test_tenant_filter_fails_closed_and_binds_the_current_tenant` |
| G-TENANT-08 | Tenant scoping state never leaks between requests: the compiled-statement cache does not capture a tenant, a pooled connection never carries a previous request's tenant criteria, and declaring a new TenantScoped model invalidates the guard's memoised table set. | `sandloom/core/tests/test_tenant_criteria_prebuilt.py::test_compiled_cache_does_not_capture_the_tenant`<br>`sandloom/core/tests/test_tenant_criteria_prebuilt.py::test_previous_requests_tenant_never_leaks_on_one_connection`<br>`sandloom/core/tests/test_tenant_guard_cache.py::test_new_tenant_model_invalidates_caches` |
| G-TENANT-09 | Queries that touch no TenantScoped table run normally with no tenant bound (the guard fails closed only on tenant tables). | `sandloom/core/tests/test_tenant_guard_gaps.py::test_non_tenant_query_without_tenant_still_works`<br>`sandloom/core/tests/test_tenant_guard_early_exit.py::test_bound_tenant_non_tenant_statement_gets_no_criteria` |
| G-TENANT-10 | TenantScope binds the tenant only from the signed access token: an X-Tenant-Id header naming a tenant outside the token's tenant\_id/org\_id claim or tenant\_ids allowlist is refused with 403, and a header inside the allowlist selects that tenant. | `sandloom/core/tests/test_tenant_isolation.py::test_header_cannot_override_token_tenant`<br>`sandloom/core/tests/test_tenant_isolation.py::test_header_can_select_tenant_from_token_allowlist`<br>`sandloom/core/tests/test_tenant_isolation.py::test_tenant_ids_parse_is_memoised_per_verified_token` |
| G-TENANT-11 | A signed tenant\_ids claim containing a malformed entry fails every request with 400 'invalid tenant id' (never memoised, no string-membership shortcut). | `sandloom/core/tests/test_tenant_isolation.py::test_malformed_signed_tenant_ids_entry_is_400_on_every_request`<br>`sandloom/core/tests/test_tenant_isolation.py::test_tenant_ids_memo_is_bounded` |
| G-TENANT-12 | A required TenantScope with no resolvable tenant answers 400 'tenant context required', a token granting several tenants without an X-Tenant-Id selector answers 400 instead of picking one, and a MissingTenantError reaching the request boundary also answers 400 'tenant context required' (never a bare 500, never the internal message). | `tests/integration/test_request_boundaries.py::test_a_missing_tenant_at_the_boundary_is_the_tenant_scope_400`<br>`tests/integration/test_request_boundaries.py::test_tenant_scope_without_a_tenant_answers_400`<br>`tests/integration/test_request_boundaries.py::test_a_token_granting_several_tenants_needs_the_selector` |
| G-TENANT-13 | TenantScope rejects a revoked access token (401) before selecting a tenant from its claims, so a stale tenant\_ids grant cannot keep selecting a tenant after revocation. | `sandloom/core/tests/test_auth_rotation_revocation.py::test_revoked_token_is_rejected_by_every_framework_guard`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_idempotency_and_tenant_scope_ignore_non_access_bearers` |
| G-TENANT-14 | With TENANCY\_MODE=single, settings refuse to load without SINGLE\_TENANT\_ID, TenantScope binds the canonical tenant, and an X-Tenant-Id selector or token tenant naming another tenant is refused with 403. | `sandloom/core/tests/test_tenant_isolation.py::test_single_tenant_mode_requires_canonical_tenant`<br>`sandloom/core/tests/test_tenant_isolation.py::test_single_tenant_mode_binds_canonical_tenant`<br>`sandloom/core/tests/test_tenant_isolation.py::test_single_tenant_mode_rejects_other_tenant_selector`<br>**gap (S)**: The 'token tenant does not match the configured tenant' 403 (token claim, no header) branch has no test. |
| G-TENANT-15 | With TENANCY\_MODE=single, request UoWs, worker/script UoWs and contract read scopes bind the canonical tenant automatically, and only for their own extent. | `sandloom/core/tests/test_tenant_isolation.py::test_single_tenant_runtime_scope_binds_canonical_tenant`<br>`sandloom/core/tests/test_open_uow.py::test_single_tenant_scope_bound_only_inside`<br>`tests/integration/test_planned_reads_tenancy_mode.py::test_read_scopes_bind_the_configured_tenant_like_the_uow` |
| G-TENANT-16 | sandloom check rule tenant-scoped-models reports every ORM model or Core Table declaring its own tenant\_id without TenantScoped, unless listed in tenant\_scope\_exempt with a written reason; a stale exemption or one without a reason fails. | `tests/architecture/test_tenant_scoped_models_rule.py::test_fixture_violations_detected`<br>`tests/architecture/test_tenant_scoped_models_rule.py::test_rule_runs_in_sandloom_check_and_honours_exemptions`<br>`tests/architecture/test_tenant_scoped_models_rule.py::test_exemption_requires_a_reason`<br>rule `tenant-scoped-models` |
| G-TENANT-17 | sandloom check rule unsafe-tenant-sql reports runtime raw SQL in business modules (text(), Model.**table**, exec\_driver\_sql, including deferred lambdas/generators) unless it executes inside an explicit unscoped() block; declaration-time defaults are exempt. | `sandloom/core/tests/test_review_archcheck.py::test_raw_tenant_sql_requires_explicit_system_scope`<br>`sandloom/core/tests/test_review_archcheck.py::test_deferred_sql_does_not_inherit_creation_scope`<br>`sandloom/core/tests/test_review_archcheck.py::test_raw_sql_declaration_defaults_do_not_need_a_runtime_scope`<br>`tests/architecture/test_archcheck_library.py::test_framework_manifest_passes_every_rule`<br>rule `unsafe-tenant-sql`<br>**gap (S)**: The rule is only tested at the analyzer level (find\_unsafe\_tenant\_sql); no test runs it through run\_checks/sandloom check on a module fixture as the tenant-scoped-models rule is. |
| G-TENANT-18 | monotonic\_upsert requires a tenant\_id in the row values, never overwrites another tenant's row with the same key, and raises TenantMismatchError when the row's tenant\_id differs from the bound tenant; monotonic\_update is scoped to the bound tenant. | `sandloom/core/tests/test_projection_upsert_guards.py::test_upsert_requires_tenant_and_never_crosses_tenants`<br>`sandloom/core/tests/test_projection_upsert_guards.py::test_update_is_scoped_to_bound_tenant` |

### G-RLS

<table> <thead> <tr> <th>Id</th> <th>Statement</th> <th>Enforced by</th> </tr> </thead> <tbody> <tr> <td><span class="sl-id">G-RLS-01</span></td> <td>On a tenant_rls database, every SQL path from the app role (ORM, Core via session/connection, text(), exec_driver_sql, streaming cursors, run_sync, executemany/insertmanyvalues, savepoints) sees and changes only rows of the tenant in sandloom.tenant_id, and an INSERT/UPDATE for another tenant is refused by WITH CHECK.</td> <td><code>sandloom/core/tests/test_tenant_rls.py::test_raw_text_sees_only_bound_tenant</code><br><code>sandloom/core/tests/test_tenant_rls.py::test_with_check_blocks_cross_tenant_writes</code><br><code>sandloom/core/tests/test_tenant_rls_paths.py::test_cross_tenant_insert_rejected_on_every_path</code><br><code>sandloom/core/tests/test_tenant_rls_paths.py::test_update_and_delete_see_only_bound_tenant</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-02</span></td> <td>An unset, empty or malformed (not canonical lowercase UUID) sandloom.tenant_id yields zero rows and refused writes on an RLS table, never a cast error or fail-open, including when no engine hook is installed.</td> <td><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_malformed_tenant_setting_fails_closed</code><br><code>sandloom/core/tests/test_tenant_rls_paths.py::test_no_tenant_fails_closed_for_every_operation</code><br><code>sandloom/core/tests/test_tenant_rls.py::test_policy_without_engine_hook_fails_closed</code><br><code>tests/perf_budgets/test_rls_merged_begin.py::test_malformed_tenant_in_context_sends_nothing</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-03</span></td> <td>The RLS policy has no bypass: neither unscoped() nor the legacy sandloom.tenant_bypass setting lets the app role read or write another tenant's rows.</td> <td><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_bypass_setting_cannot_read_other_tenants</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_bypass_setting_cannot_write_other_tenants</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_unscoped_never_reaches_other_tenants_on_app_role</code><br><code>sandloom/core/tests/test_tenant_rls_paths.py::test_legacy_bypass_setting_is_inert_on_every_path</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-04</span></td> <td>sandloom.tenant_id is transaction-local and re-synced by the engine hook: it follows a tenant switch within a transaction, is restored after a savepoint rollback, and never carries one pooled transaction's tenant into the next.</td> <td><code>sandloom/core/tests/test_tenant_rls.py::test_settings_do_not_leak_across_pooled_transactions</code><br><code>sandloom/core/tests/test_tenant_rls.py::test_settings_resync_within_one_transaction</code><br><code>sandloom/core/tests/test_tenant_rls.py::test_savepoint_rollback_forces_resync</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_crud_is_tenant_scoped_and_pool_reuse_does_not_leak</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-05</span></td> <td>Startup refuses (TenantRLSConfigError, in every environment, aborting aopen_all and Celery worker boot) a tenant_rls database whose application role is a superuser, has BYPASSRLS, is a member of such a role, owns an RLS table directly or by membership, or is covered by an unconditional (system-role) policy.</td> <td><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_verification_refuses_superuser_app_role</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_verification_refuses_owner_and_member</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_verification_failure_aborts_startup_in_every_environment</code><br><code>sandloom/core/tests/test_worker.py::test_worker_boot_runs_tenant_rls_role_verification</code><br><strong>gap (S)</strong>: The BYPASSRLS-attribute branch (and membership in a BYPASSRLS/superuser role) has no direct test; only SUPERUSER, table ownership and system-role membership are exercised.</td> </tr> <tr> <td><span class="sl-id">G-RLS-06</span></td> <td>The role check cannot be switched off: the former verify_roles database option is refused at startup, and a hardened NOSUPERUSER NOBYPASSRLS role passes.</td> <td><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_removed_verify_roles_option_is_refused</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_verification_accepts_hardened_roles</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-07</span></td> <td>enable_tenant_rls(op, table) emits (through the Alembic op) ENABLE + FORCE row level security and the <table>_tenant_isolation policy, is idempotent, and disable_tenant_rls reverts it; the tenant setting SQL is parameterised.</table></td> <td><code>sandloom/core/tests/test_tenant_rls.py::test_migration_helper_emits_policy_through_op</code><br><code>sandloom/core/tests/test_tenant_rls.py::test_disable_reverts_policy</code><br><code>sandloom/core/tests/test_tenant_rls.py::test_install_is_idempotent_and_postgres_only</code><br><code>sandloom/core/tests/test_tenant_rls.py::test_set_config_sql_is_parameterised</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-08</span></td> <td>The system channel (system_session) reaches other tenants only on tables migrated with enable_tenant_rls(..., system_access=True), stays tenant-scoped on every other RLS table, honours an explicit tenant_id, rolls back on error, and writes an sandloom.system_db.session audit row with actor and reason in the same transaction.</td> <td><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_system_channel_reaches_all_tenants_and_is_audited</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_system_session_tenant_scoped_and_rollback</code><br><strong>gap (S)</strong>: 'No audit row, no commit' (a failing audit write aborts the system session's commit) and the structured start/end log record (op_id, outcome, duration) are not tested.</td> </tr> <tr> <td><span class="sl-id">G-RLS-09</span></td> <td>system_session refuses (SystemSessionRefused) an empty actor or a too-short reason and raises SystemChannelUnavailable for a database with no system channel; require_system_channel without a system DSN, or a system_url without tenant_rls, fails at provider construction.</td> <td><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_system_session_requires_actor_reason_and_configuration</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_require_system_channel_without_dsn_fails_fast</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-10</span></td> <td>Business-module code has no path to the system channel: sandloom.core.drivers (system_db) and sandloom.core.sql_alchemy.tenant_rls are forbidden infra imports, absent from the public core API, and the database provider exposes no system engine; the app role cannot SET ROLE to the system role.</td> <td><code>tests/architecture/test_system_channel_unreachable.py::test_module_import_of_system_channel_is_flagged</code><br><code>tests/architecture/test_system_channel_unreachable.py::test_system_channel_not_in_public_core_api</code><br><code>tests/architecture/test_system_channel_unreachable.py::test_provider_does_not_expose_system_engine</code><br><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_app_role_cannot_assume_system_role</code><br>rule <code>infra-imports</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-11</span></td> <td>A raw asyncpg driver connection bypasses the RLS hook but runs under the transaction's last synced setting, so with no tenant it fails closed (zero rows).</td> <td><code>sandloom/core/tests/test_tenant_rls_paths.py::test_raw_driver_connection_is_not_covered_but_fails_closed</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-12</span></td> <td>The in-process contract pool installs the RLS hook too, so contract_session() statements are scoped by sandloom.tenant_id on a tenant_rls database (also when tenant_rls is requested after the default contract engine exists).</td> <td><code>sandloom/core/tests/test_contract_pool.py::test_default_contract_engine_honours_a_later_tenant_rls_request</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-13</span></td> <td>The no-tenant round-trip-skipping allowlist never weakens RLS: it never skips a bound tenant, clears a tenant set earlier in the transaction, ends with its block, refuses untagged raw SQL or a tenant table inside it (NonTenantStatementError), and in production syncs instead of skipping on a violation.</td> <td><code>sandloom/core/tests/test_tenant_rls_allowlist.py::test_allowlist_never_skips_a_bound_tenant</code><br><code>sandloom/core/tests/test_tenant_rls_allowlist.py::test_a_tenant_table_in_the_scope_is_refused</code><br><code>sandloom/core/tests/test_tenant_rls_allowlist.py::test_untagged_raw_sql_in_the_scope_is_refused</code><br><code>sandloom/core/tests/test_tenant_rls_allowlist.py::test_in_production_a_violation_is_synced_never_skipped</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-14</span></td> <td>Residual risk is pinned, not hidden: forging sandloom.tenant_id via set_config under the shared app role reads another tenant (strict xfail), and a superuser bypasses RLS (documented caveat test); a fix that changes either must flip these tests.</td> <td><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_forged_tenant_setting_cannot_read_other_tenant</code><br><code>sandloom/core/tests/test_tenant_rls.py::test_superuser_bypasses_rls_documented_caveat</code></td> </tr> <tr> <td><span class="sl-id">G-RLS-15</span></td> <td>A database instance without tenant_rls is unchanged: no hook, no system channel, no I/O at aopen, and the provider does not advertise the tenant_rls feature (it is advertised only when the option is on).</td> <td><code>sandloom/core/tests/test_tenant_rls_bypass.py::test_rls_off_instance_is_unchanged</code><br><code>sandloom/core/tests/test_tenant_rls_paths.py::test_tenant_rls_off_changes_nothing</code><br><code>sandloom/core/tests/test_database_tenant_rls_feature.py::test_provider_advertises_tenant_rls_only_when_enabled</code></td> </tr> </tbody> </table>

### G-PROFILE

<table> <thead> <tr> <th>Id</th> <th>Statement</th> <th>Enforced by</th> </tr> </thead> <tbody> <tr> <td><span class="sl-id">G-PROFILE-01</span></td> <td>With ENVIRONMENT=prod and PRODUCTION_PROFILE=multi_tenant_strict, boot refuses (enforce_configuration) TENANCY_MODE other than multi, any database instance without tenant_rls, AUTH_REVOCATION_FAIL_CLOSED=false, and a SECURITY_CACHE_INSTANCE that is 'default', undeclared, or on the same Redis server as the default cache — listing every reason.</td> <td><code>sandloom/core/tests/test_production_profile.py::test_default_production_config_is_refused_with_every_reason</code><br><code>sandloom/core/tests/test_production_profile.py::test_each_trust_layer_is_required</code></td> </tr> <tr> <td><span class="sl-id">G-PROFILE-02</span></td> <td>The strict profile's boot check stops every entry point — ASGI app, Celery worker, sandloom migrate and the sandloom CLI — before it serves or touches anything.</td> <td><code>sandloom/core/tests/test_boot_configuration_checks.py::test_moved_validator_refuses_every_entry_point</code></td> </tr> <tr> <td><span class="sl-id">G-PROFILE-03</span></td> <td>Under the strict profile AUTH_REVOCATION_FAIL_CLOSED is turned on when unset, and a compliant configuration starts.</td> <td><code>sandloom/core/tests/test_production_profile.py::test_compliant_strict_profile_starts_and_forces_fail_closed</code></td> </tr> <tr> <td><span class="sl-id">G-PROFILE-04</span></td> <td>The strict profile is inert outside ENVIRONMENT=prod and with PRODUCTION_PROFILE=standard.</td> <td><code>sandloom/core/tests/test_production_profile.py::test_profile_is_inert_outside_prod_and_for_standard</code></td> </tr> <tr> <td><span class="sl-id">G-PROFILE-05</span></td> <td>Under the strict profile a critical startup task (API and worker) fails with ProductionProfileError, naming each table and the enable_tenant_rls(op, table) fix, when any TenantScoped table on any database lacks enabled+FORCEd RLS or its <table>_tenant_isolation policy, or is missing.</table></td> <td><code>sandloom/core/tests/test_production_profile.py::test_rls_coverage_reports_unprotected_tenant_tables</code><br><code>sandloom/core/tests/test_production_profile.py::test_startup_enforcement_fails_on_an_uncovered_table</code><br><strong>gap (S)</strong>: enforce_runtime is tested directly, but no test proves it is wired as a critical startup task of create_app's lifespan or of the Celery worker boot (removing either call site would stay green).</td> </tr> <tr> <td><span class="sl-id">G-PROFILE-06</span></td> <td>Under the strict profile startup attests via INFO server identity that the security cache (and a separate idempotency cache) reach a different Redis process than the default cache, failing with ProductionProfileError on the same process or when identity cannot be read.</td> <td><code>sandloom/core/tests/test_infra_review_20261022.py::test_strict_profile_checks_physical_redis_identity</code><br><code>sandloom/core/tests/test_idempotency_cache_instance.py::test_strict_runtime_attests_the_idempotency_redis_process</code><br><strong>gap (S)</strong>: The 'Cannot attest separate ... Redis processes' failure (deployment_identity raising) has no test.</td> </tr> <tr> <td><span class="sl-id">G-PROFILE-07</span></td> <td>Under the strict profile an IDEMPOTENCY_CACHE_INSTANCE set to another instance than the security cache must be declared and on a different Redis server than the default cache (refused otherwise), sharing the security Redis is only a warning, and leaving it unset keeps the previous behaviour.</td> <td><code>sandloom/core/tests/test_idempotency_cache_instance.py::test_strict_profile_refuses_an_unsafe_idempotency_instance</code><br><code>sandloom/core/tests/test_idempotency_cache_instance.py::test_strict_profile_warns_when_sharing_the_security_redis</code><br><code>sandloom/core/tests/test_idempotency_cache_instance.py::test_strict_profile_accepts_a_separate_idempotency_redis</code><br><code>sandloom/core/tests/test_idempotency_cache_instance.py::test_strict_profile_default_is_unchanged</code></td> </tr> <tr> <td><span class="sl-id">G-PROFILE-08</span></td> <td>Security-critical state (revocation marks, blocks, idempotency records) is written through SECURITY_CACHE_INSTANCE, never plain cache(); idempotency records move to IDEMPOTENCY_CACHE_INSTANCE when set while revocation marks stay in the security cache.</td> <td><code>sandloom/core/tests/test_production_profile.py::test_security_state_uses_the_security_cache_instance</code><br><code>sandloom/core/tests/test_idempotency_cache_instance.py::test_revocation_marks_stay_in_the_security_cache</code><br><code>sandloom/core/tests/test_idempotency_cache_instance.py::test_reserve_replay_conflict_release_use_the_idempotency_instance</code></td> </tr> <tr> <td><span class="sl-id">G-PROFILE-09</span></td> <td>Under the strict profile a split deployment (SERVICE_BINDINGS set) is refused with CONTRACT_COMPAT_CHECK=off or a shared HS secret (the accepted risk shared_hs_secret_split is refused), and a process loading no token issuer is refused an HS secret by the kernel role check with no opt-out.</td> <td><code>sandloom/core/tests/test_production_profile.py::test_each_trust_layer_is_required</code><br><code>sandloom/core/tests/test_production_profile.py::test_split_requires_asymmetric_jwt</code><br><code>sandloom/core/tests/test_production_profile.py::test_startup_role_check_has_no_shared_secret_opt_out_under_the_profile</code></td> </tr> </tbody> </table>

### G-UOW

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-UOW-01 | A Unit of Work commits when its body succeeds and rolls back (dispatching no outbox rows) when the body raises any exception, HTTPException included. | `sandloom/core/tests/test_open_uow.py::test_commit_then_dispatch`<br>`sandloom/core/tests/test_open_uow.py::test_body_error_rolls_back_without_dispatch`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_rolled_back_request_leaves_no_record`<br>`sandloom/core/tests/test_fused_effect_rows.py::test_rolled_back_uow_writes_nothing` |
| G-UOW-02 | Every framework UoW dependency (DbSession, Database(name), get\_db) is declared with scope="function", so the request transaction commits (and its on-commit hooks run) before the response is sent. | `sandloom/core/tests/test_uow_commits_before_response.py::test_dbsession_and_database_markers_use_function_scope`<br>`sandloom/core/tests/test_uow_commits_before_response.py::test_every_framework_get_db_dependency_declares_function_scope`<br>`sandloom/core/tests/test_uow_commits_before_response.py::test_db_session_dependency_exits_before_the_response_starts` |
| G-UOW-03 | Only the UnitOfWorkContract issues COMMIT and only the commit pipeline owners reach its stages; business modules never call db.commit()/db.rollback() (uow-boundary rule). | `tests/architecture/test_commit_pipeline.py::test_only_the_uow_contract_commits_an_async_session`<br>`tests/architecture/test_commit_pipeline.py::test_pipeline_stages_are_reached_only_through_their_owners`<br>`tests/architecture/test_no_uow_boundary_breaks.py::test_module_does_not_break_uow_boundary`<br>`tests/architecture/test_no_uow_boundary_breaks.py::test_analyzer_detects_boundary_break`<br>rule `uow-boundary` |
| G-UOW-04 | The commit pipeline runs in the documented order before\_commit -\> COMMIT -\> outbox dispatch -\> on\_commit, and on\_commit callbacks run after the outbox stage (also when there is no outbox stage). | `tests/architecture/test_commit_pipeline.py::test_the_pipeline_is_documented_in_order`<br>`sandloom/core/tests/test_open_uow.py::test_pipeline_runs_on_commit_after_the_outbox_stage`<br>`sandloom/core/tests/test_open_uow.py::test_pipeline_without_outbox_stage_still_runs_on_commit`<br>`sandloom/core/tests/test_before_commit.py::test_checks_run_in_order_after_the_handler_and_before_commit` |
| G-UOW-05 | Rolling back a SAVEPOINT (db.begin\_nested()) discards the outbox rows, on\_commit callbacks and before\_commit checks registered inside it, while those of surviving savepoints and the parent stay live. | `sandloom/core/tests/test_savepoint_tracking.py::test_rolled_back_savepoint_kills_its_token_and_survivors_live`<br>`sandloom/core/tests/test_kernel_hardening.py::test_publish_in_rolled_back_savepoint_is_not_dispatched`<br>`sandloom/core/tests/test_fused_effect_rows.py::test_written_rows_of_a_rolled_back_savepoint_are_not_dispatched`<br>`sandloom/core/tests/test_on_commit.py::test_rolled_back_savepoint_drops_its_callbacks` |
| G-UOW-06 | When several database sessions are active in one scope, publish\_event and record\_audit without database= raise instead of guessing, and an explicit database= deterministically selects that database's UoW (each database owns its own outbox). | `sandloom/core/tests/test_review_fixes_v2.py::test_resolve_scoped_session_ambiguous_without_database`<br>`sandloom/core/tests/test_review_fixes_v2.py::test_resolve_scoped_session_single_database`<br>**gap (S)**: Only the private resolver is unit-tested; no end-to-end test holds DbSession + Database("x") in one endpoint and asserts the event lands in the chosen database's outbox\_messages (and is marked delivered there). |
| G-UOW-07 | Two named databases are independent engines with independent UoWs; there is no shared COMMIT across them (no distributed transaction). | `sandloom/core/tests/test_infra_multi_database.py::test_two_databases_are_independent_and_both_work`<br>**gap (S)**: Non-atomicity itself is a documented non-guarantee; no test pins that a failing second-database UoW leaves the first committed (i.e. that nobody quietly introduces a coupled commit). |
| G-UOW-08 | The default-database UoW binds the single-tenant canonical tenant only inside the UoW and a tenant-scope failure on enter rolls the UoW back. | `sandloom/core/tests/test_open_uow.py::test_single_tenant_scope_bound_only_inside`<br>`sandloom/core/tests/test_open_uow.py::test_tenant_scope_failure_on_enter_rolls_back` |

### G-UNIT

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-UNIT-01 | db() is the session of the innermost open unit of the current context; outside any unit it is SANDLOOM-R002; the first db() in a request creates the lazy request unit (handler task only), committed on success and rolled back on error before the response; a closed unit refuses reuse and never auto-begins (an exited unit's session becomes a ClosedKernelSession: every mutation and I/O entry refuses with SANDLOOM-R002, independent of DB\_SESSION\_OWNER\_CHECKS). | `tests/integration/test_units_and_admission.py::test_db_outside_any_unit_is_refused`<br>`tests/integration/test_units_and_admission.py::test_db_in_a_block_is_that_units_session`<br>`tests/integration/test_units_and_admission.py::test_lazy_request_unit_commits_on_success_and_rolls_back_on_error`<br>`tests/integration/test_units_and_admission.py::test_child_db_before_the_parent_creates_nothing_then_parent_owns`<br>`tests/integration/test_units_and_admission.py::test_child_open_uow_before_the_parent_works`<br>`tests/integration/test_units_and_admission.py::test_an_exited_unit_refuses_reuse_and_never_autobegins` |
| G-UNIT-02 | Operation-intent mode: no request unit, sequential units; external\_call() and release\_check() exclude any unit, read-scope connection or checkout during their lifetime, the window opening atomically with the no-unit check and re-verified after the read-scope drain, so a unit a racing task opens meanwhile is refused (SANDLOOM-R008; the authority callback's read-contract calls excepted); the drain waits for a read contract already running on the scope's shared connection and closes its unit (a window entered from inside that read is SANDLOOM-R008); the window also waits for every contract call of the request already running in its provider unit (command or read; refused from inside one) and refuses a new one while open, so it never runs beside a provider transaction; a REMOTE contract call (the generated RPC proxy) obeys the same rules: refused inside a window before it is sent, counted while it runs so a window entered meanwhile waits for it (local == remote), and a remote call with an attempt its provider never answered (a timeout, a transport failure: its provider unit may still run after the transport gave up) refuses every later window of the request, fail closed (503, SANDLOOM-R008; the client retries with its Idempotency-Key), while an answered failure leaves it open, "answered" meaning its MAC-bound effects were merged (round 5): an answer whose fx the caller never merged - one larger than RPC\_MAX\_RESPONSE\_BYTES (its fx.id lost with it), a reply without a well-formed envelope, malformed fx, or an unkeyed (unauthenticated) refusal - counts as unanswered (round 6: fx is malformed when any field is unknown or any ENTRY of rk / id / sr / ra / su has the wrong type or exceeds its bound - fx.id \[None\], a 17th id, an unknown risk, a non-positive step-up, an ra other than exactly the integer 1 (round 7: 1.0 and true are malformed) -; its well-formed risks still mark the request, its ra never does), so a root catching that 502 / 503 cannot open a window beside a nested unit that may still run; at any depth: a provider (or a local contract) whose own nested remote call went unanswered reports it back MAC-bound (fx.id) even when it caught the failure and answered OK, and every caller up the chain merges it, so the root's windows refuse too (a replay of that provider's command does not re-raise it: the uncertainty is not a committed effect); a 2xx outside the issue case (at least one external call, release\_check after the last, exactly one write after it) or the issued-retry case (no commit after the release) is discarded with SANDLOOM-R012; an organization account needs the authority; db() is SANDLOOM-R002; SANDLOOM-B019 at startup. | `tests/integration/test_route_runtime.py::test_an_intent_2xx_without_release_check_is_discarded`<br>`tests/integration/test_route_runtime.py::test_two_writes_after_the_release_check_are_discarded`<br>`tests/integration/test_route_runtime.py::test_the_issue_and_issued_retry_cases_pass_and_hold_no_connection`<br>`tests/integration/test_route_runtime.py::test_release_check_for_an_org_needs_the_authority_and_db_is_refused`<br>`tests/integration/test_route_runtime.py::test_capability_replay_checks_at_startup`<br>`sandloom_billing/tests/test_operation_intents.py`<br>`sandloom_billing/tests/test_intent_reconcile.py`<br>`sandloom_billing/tests/test_f_class_authority.py`<br>`sandloom/core/tests/test_intent_window_race.py::test_a_unit_racing_the_window_entry_is_refused`<br>`sandloom/core/tests/test_intent_window_race.py::test_a_unit_left_open_by_the_drain_refuses_the_window`<br>`sandloom/core/tests/test_contract_read_scope.py::test_an_intent_window_waits_for_a_running_read_and_closes_its_unit`<br>`sandloom/core/tests/test_contract_read_scope.py::test_an_intent_window_inside_a_running_read_is_refused`<br>`sandloom/core/tests/test_contract_read_scope.py::test_an_intent_window_never_runs_beside_a_running_provider_unit`<br>`sandloom/core/tests/test_rpc_intent_window.py::test_a_remote_command_inside_an_intent_window_is_refused`<br>`sandloom/core/tests/test_rpc_intent_window.py::test_an_intent_window_waits_for_a_running_remote_command`<br>`tests/integration/test_rpc_caller_parity.py::test_no_window_opens_while_a_timed_out_remote_command_may_run`<br>`tests/integration/test_rpc_caller_parity.py::test_an_answered_remote_failure_leaves_the_window_open`<br>`tests/integration/test_rpc_caller_parity.py::test_a_nested_remote_timeout_keeps_the_root_in_doubt`<br>`tests/integration/test_rpc_caller_parity.py::test_a_local_wrapper_catching_a_remote_timeout_leaves_the_request_in_doubt`<br>`tests/integration/test_rpc_caller_parity.py::test_an_oversized_answer_keeps_the_nested_doubt`<br>`tests/integration/test_rpc_caller_parity.py::test_an_answer_whose_effects_were_not_merged_is_in_doubt`<br>`tests/integration/test_rpc_caller_parity.py::test_a_well_formed_answer_settles_the_call` |
| G-UNIT-03 | Write admission covers the cursor boundary: a write through the session's connection (conn.exec\_driver\_sql, also under unscoped()) is classified and refused like an ORM or Core write when another unit of the request is the writer. | `tests/integration/test_units_and_admission.py::test_a_second_writing_unit_is_refused_before_the_cursor`<br>`tests/integration/test_units_and_admission.py::test_classification_of_core_constructs` |

### G-MSTORE

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-MSTORE-01 | The write store of a route is resolved statically: a single writable session is the write store; create\_app refuses a write route with several writable sessions and neither writes() nor allow\_multi\_db\_write, and a writes() naming a database the route does not inject (SANDLOOM-B009), and an allow\_multi\_db\_write secondary that is the write store or not injected (SANDLOOM-B013). | `tests/integration/test_write_store.py::test_several_writable_sessions_need_a_write_store` |
| G-MSTORE-02 | A money-ledger insert in an opted-out (allow\_multi\_db\_write) secondary is refused (SANDLOOM-B014); the same insert in the route's write store is admitted. | `tests/integration/test_write_store.py::test_a_money_effect_in_an_opted_out_secondary_is_refused` |
| G-MSTORE-03 | One writer per request: the first unit that writes becomes the writer; a write by any other unit of the request is refused before the cursor (SANDLOOM-R007), unflushed changes at COMMIT included; a read-only parent with a child's sole write is allowed, a child and its parent never both write; declared secondaries (allow\_multi\_db\_write) are admitted, before or after the write store (a secondary is never the writer), and commit first, the write store last, whatever the endpoint's parameter order and however the sessions are nested in dependencies (either order, any depth, a session shared with the endpoint); a later commit failing after a writing unit of the request committed (a partial commit) increments sandloom\_multi\_store\_partial\_commit\_total{route,store} (the route template, each committed store) and logs a warning naming the route, the committed stores and the failing store, while a request that commits everything or nothing is never counted; outside a request units are unconstrained. | `tests/integration/test_units_and_admission.py::test_a_second_writing_unit_is_refused_before_the_cursor`<br>`tests/integration/test_units_and_admission.py::test_an_unflushed_change_in_a_second_unit_is_refused_at_commit`<br>`tests/integration/test_units_and_admission.py::test_the_first_writer_wins_even_if_it_opened_later`<br>`tests/integration/test_units_and_admission.py::test_read_only_parent_and_a_childs_sole_write_is_allowed`<br>`tests/integration/test_units_and_admission.py::test_a_child_and_its_parent_never_both_write`<br>`tests/integration/test_units_and_admission.py::test_outside_a_request_units_are_unconstrained`<br>`tests/integration/test_units_and_admission.py::test_declared_secondaries_are_admitted`<br>`tests/integration/test_units_and_admission.py::test_a_secondary_written_before_the_write_store_is_admitted`<br>`tests/integration/test_route_runtime.py::test_the_secondary_commits_before_the_write_store_whatever_the_order`<br>`tests/integration/test_route_runtime.py::test_a_write_store_commit_failing_after_the_secondary_committed_is_counted`<br>`tests/integration/test_units_and_admission.py::test_random_unit_sequences_never_execute_a_second_writer`<br>`tests/laws/test_write_admission_laws.py::test_no_second_writer_statement_ever_executes` |
| G-MSTORE-04 | Write admission's stated scope: every SQLAlchemy statement through a kernel session or connection is classified (transaction-control statements are not writes; raw DBAPI is unavailable to modules); contract, consumer, task, saga-step and kernel units run in a fresh admission scope. | `tests/integration/test_units_and_admission.py::test_classification_of_core_constructs`<br>`tests/integration/test_units_and_admission.py::test_random_unit_sequences_never_execute_a_second_writer`<br>`tests/laws/test_write_admission_laws.py::test_no_second_writer_statement_ever_executes` |
| G-MSTORE-05 | create\_app refuses (StoreCapabilityError, SANDLOOM-B010) a write route whose write store lacks audit while the route carries a static risk, a route with an explicit durable idempotency guard whose default database lacks http\_idempotency, and any database hosting request units whose driver lacks write\_admission; the check reads the registry's advertised features without building a provider. | `tests/integration/test_write_store.py::test_a_write_store_without_a_default_feature_is_refused_at_boot`<br>`tests/integration/test_write_store.py::test_a_database_hosting_request_units_needs_write_admission` |
| G-MSTORE-06 | create\_app refuses (SANDLOOM-B011) a write route with a writable session on a store without transactions (it cannot commit with the request unit), unless that database is a declared allow\_multi\_db\_write secondary. | `tests/integration/test_write_store.py::test_a_non_transactional_store_in_a_write_route_is_refused` |
| G-MSTORE-07 | Consistency tokens never become an error: SANDLOOM-Consistency-Token is HMAC-SHA256 under PROJECTION\_TOKEN\_KEYS (the newest kid signs, every listed kid verifies), expires after 600 s, carries at most 16 entries (a larger write gets none) and is ignored when missing, malformed, forged, expired, of an unknown kid or of another tenant; a write route whose pending events feed a projection answers with the token of the rows they produce, each entry naming its source group and aggregate; a read presenting it waits at most wait\_ms (PROJECTION\_RYW\_WAIT\_MS) until each entry's own group version column reaches it (never another group's version; a page read rereads its rows every poll round and a relational read reloads the rows its session already holds, so a cached row behind the token catches up within the budget), then serves with SANDLOOM-Projection-Stale: true (on\_stale="serve") or reads the entity from its source (on\_stale="source"). | `sandloom/core/tests/test_projection_kernel.py::test_a_token_round_trips_under_the_newest_kid_and_any_listed_kid`<br>`sandloom/core/tests/test_projection_kernel.py::test_a_bad_token_is_ignored_never_an_error`<br>`sandloom/core/tests/test_projection_kernel.py::test_a_large_write_gets_no_token_and_no_key_means_no_token`<br>`sandloom/core/tests/test_projection_kernel.py::test_a_read_waits_only_for_its_own_projections_missing_versions`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_write_token_names_the_rows_its_events_produce`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_write_route_sets_the_token_header`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_read_waits_for_the_token_then_serves_stale`<br>`tests/sdk/test_sdk_projection_ops.py::test_the_token_digest_is_the_kernels`<br>`sandloom/core/tests/test_projection_kernel.py::test_a_wait_compares_each_source_group_with_its_own_version`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_token_of_one_group_is_not_satisfied_by_another_groups_version`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_store_token_compares_its_own_groups_version`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_waiting_read_rereads_until_its_group_catches_up`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_waiting_relational_read_reloads_the_rows_it_already_holds` |
| G-MSTORE-08 | Projection apply converges: full-state events replace a group's owned fields iff the stored version is NULL or older, so any interleaving with duplicates ends at the max-version event per group, on a relational model and on a conditional\_write store alike (an independent group's upsert never clears another group's tombstone); every column has exactly one owning group (SANDLOOM-B012); delta mappers are accepted only for a relational projection under the durable inbox (exactly once by the inbox) and refused on a store target. | `tests/laws/test_projection_laws.py::test_full_state_converges_to_the_max_version_event_per_group`<br>`tests/laws/test_projection_laws.py::test_a_column_has_exactly_one_owner_or_the_projection_is_refused`<br>`tests/laws/test_projection_laws.py::test_a_delta_mapper_is_accepted_only_under_the_durable_inbox`<br>`tests/laws/test_projection_laws.py::test_deltas_under_the_inbox_count_each_event_exactly_once`<br>`tests/sdk/test_sdk_projections.py::test_full_state_converges_under_shuffles_and_duplicates`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_store_projection_converges_and_reads_skip_tombstones`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_store_target_refuses_deltas_and_two_targets`<br>`tests/sdk/test_sdk_projection_ops.py::test_an_independent_group_never_resurrects_a_store_tombstone` |
| G-MSTORE-09 | Tombstones are versioned and beat any late older upsert; only a newer write of the group that owns deletion clears one (never another group's upsert, on a relational model and a store target alike, as the conditional\_write conformance kit checks); they are purged only by purge\_tombstones(name, older\_than=) / sandloom projection purge, which refuses an older\_than below the replay horizon (stream retention + relay horizon, the oldest DLQ entry and the oldest dead outbox row of the source channels; never with an unbounded retention) and while any rebuild runs (running rebuilds are counted, so one finishing never lifts the exclusion of another; a rebuild's entry and a purge's rebuild check, watermark raise and delete are mutually exclusive - the purge window, a per-projection transaction advisory lock the relational begin\_rebuild takes too, an asyncio lock in memory - so a rebuild that starts after the purge read no rebuild waits until the delete is done, and a purge after a begun rebuild is refused), raises the monotonic purged\_before watermark before deleting (a rebuild never lowers it), and from then on the apply path dead-letters an event emitted before the watermark (fail closed without emitted\_at); the apply''s watermark check and its write are atomic with respect to a purge (the apply''s unit holds the watermark row FOR SHARE, created when missing, until it commits; the raise row-locks it), so a purge racing an apply never resurrects a row, on a relational model and a store target alike. | `tests/laws/test_projection_laws.py::test_a_tombstone_beats_any_late_older_upsert`<br>`tests/sdk/test_sdk_projections.py::test_a_tombstone_beats_a_late_older_upsert`<br>`sandloom/core/tests/test_projection_kernel.py::test_a_purge_below_the_replay_horizon_is_refused`<br>`sandloom/core/tests/test_projection_kernel.py::test_the_watermark_is_raised_before_the_delete_and_never_lowered`<br>`sandloom/core/tests/test_projection_kernel.py::test_a_purge_refuses_during_a_rebuild_and_a_rebuild_keeps_the_mark`<br>`sandloom/core/tests/test_projection_kernel.py::test_an_event_older_than_the_watermark_is_dead_lettered`<br>`sandloom/core/tests/test_projection_watermarks_db.py::test_the_watermark_only_ever_rises`<br>`sandloom/core/tests/test_projection_watermarks_db.py::test_the_rebuild_flag_is_set_and_cleared_without_touching_the_mark`<br>`sandloom/core/tests/test_projection_kernel.py::test_concurrent_rebuilds_keep_the_purge_excluded_in_memory`<br>`sandloom/core/tests/test_projection_watermarks_db.py::test_concurrent_rebuilds_keep_the_purge_excluded_in_the_table`<br>`sandloom/core/tests/test_projection_kernel.py::test_a_rebuild_never_enters_a_running_purge_in_memory`<br>`sandloom/core/tests/test_projection_watermarks_db.py::test_a_rebuild_never_enters_a_running_purge_in_the_table`<br>`sandloom/core/tests/test_projection_watermarks_db.py::test_the_purge_window_holds_through_the_operations_units`<br>`sandloom/core/tests/test_projection_watermarks_db.py::test_racing_rebuild_marks_are_counted_exactly`<br>`tests/sdk/test_sdk_projection_ops.py::test_the_relational_purge_deletes_tombstones_older_than_the_cutoff`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_store_purge_deletes_old_tombstones_only`<br>`tests/sdk/test_sdk_projection_ops.py::test_an_event_older_than_the_watermark_is_refused_on_apply`<br>`tests/sdk/test_sdk_projection_ops.py::test_only_a_projection_with_tombstones_gets_the_guard`<br>`tests/sdk/test_sdk_projection_purge_race.py::test_a_purge_racing_an_apply_never_resurrects_a_relational_row`<br>`tests/sdk/test_sdk_projection_purge_race.py::test_a_purge_racing_an_apply_never_resurrects_a_store_row`<br>`tests/sdk/test_sdk_projection_ops.py::test_an_independent_group_never_resurrects_a_store_tombstone`<br>`sandloom/core/tests/test_projection_kernel.py::test_the_conformance_kit_catches_a_store_whose_upserts_clear_tombstones` |
| G-MSTORE-10 | A store projection is tenant-native: each conditional write names its row's tenant, a row whose tenant is missing or differs from the bound tenant is refused, and store reads return only the bound tenant's rows: a read key or filter naming another tenant never overrides the bound one (it reads nothing, fail closed) and a store read without a bound tenant is refused. | `tests/sdk/test_sdk_projection_ops.py::test_a_store_row_of_another_tenant_is_refused_and_reads_are_per_tenant` |

### G-OUTBOX

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-OUTBOX-01 | Inside a UoW, publish\_event writes the event to outbox\_messages in the same transaction; a rollback discards the row and nothing is published. | `sandloom/core/tests/test_uow_outbox.py::test_publish_event_enqueues_to_outbox_in_same_transaction`<br>`sandloom/core/tests/test_uow_outbox.py::test_rollback_discards_outbox_rows_and_nothing_is_published`<br>`sandloom/core/tests/test_pipelined_dispatch.py::test_rollback_publishes_nothing` |
| G-OUTBOX-02 | Outside a UoW scope publish\_event (and publish\_event\_versions) raises UnitOfWorkRequiredError; only required=False publishes straight to the bus (at-most-once), and required=False inside a UoW still uses the outbox. | `sandloom/core/tests/test_uow_outbox.py::test_publish_event_outside_uow_raises`<br>`sandloom/core/tests/test_uow_outbox.py::test_publish_event_best_effort_outside_uow_goes_straight_to_bus`<br>`sandloom/core/tests/test_uow_outbox.py::test_best_effort_inside_uow_still_uses_the_outbox`<br>`sandloom/core/tests/test_uow_outbox.py::test_publish_event_versions_outside_uow_raises_unless_best_effort` |
| G-OUTBOX-03 | In module code every best-effort downgrade (publish\_event / publish\_event\_versions / record\_audit with required=False) must carry a '\# sandloom: best-effort ' marker or sandloom check fails. | `tests/architecture/test_best_effort_effects_rule.py::test_fixture_violations_are_flagged_and_marked_calls_pass`<br>`tests/architecture/test_best_effort_effects_rule.py::test_aliased_effects_and_kwargs_splat_are_flagged`<br>`tests/architecture/test_best_effort_effects_rule.py::test_rule_runs_in_sandloom_check`<br>rule `best-effort-effects` |
| G-OUTBOX-04 | publish\_event preflights channel name, payload schema and payload size before the event is written, so an event the bus would reject is refused to the caller instead of being committed to the outbox. | `sandloom/core/tests/test_infra_hardening.py::test_preflight_rejects_oversized_payload`<br>`sandloom/core/tests/test_infra_hardening.py::test_preflight_rejects_missing_schema`<br>`sandloom/core/tests/test_strict_mode.py::test_publish_missing_schema_raises_in_every_profile`<br>`sandloom/core/tests/test_event_bus_schemas.py::test_validation_failure_lists_every_violation`<br>`sandloom/core/tests/test_uow_outbox.py::test_publish_event_best_effort_outside_uow_is_preflighted`<br>`sandloom/core/tests/test_guardrails.py::test_event_payload_size_guard`<br>**gap (S)**: No test calls publish\_event INSIDE a UoW with an invalid channel / schema-violating / oversized payload and asserts the call raises and no outbox\_messages row exists; the in-UoW path (unit\_of\_work/effects.py preflight) is only covered indirectly. |
| G-OUTBOX-05 | The dispatched envelope id equals the outbox row id and stays the same across post-commit dispatch and relay redeliveries. | `sandloom/core/tests/test_uow_outbox.py::test_committed_events_marked_delivered_after_dispatch`<br>`sandloom/core/tests/test_pipelined_dispatch.py::test_failed_batch_marks_nothing_and_relay_redelivers_same_ids` |
| G-OUTBOX-06 | The channel suffix is authoritative for the schema version (.v2 -\> schema\_version "v2"), so the outbox row and the dispatched envelope never disagree. | `sandloom/core/tests/test_review_fixes_v2.py::test_normalize_schema_version_derives_from_channel_suffix` |
| G-OUTBOX-07 | A failed post-commit dispatch never fails the committed operation: the row stays pending and the relay delivers it later (rows are marked delivered iff delivered; a crash between publish and mark leads to redelivery, never loss). | `sandloom/core/tests/test_uow_outbox.py::test_dispatch_failure_leaves_message_pending_for_relay`<br>`sandloom/core/tests/test_open_uow.py::test_post_commit_dispatch_failure_never_fails_committed_uow`<br>`sandloom/core/tests/test_post_commit_dispatcher.py::test_rows_are_marked_iff_delivered`<br>`sandloom/core/tests/test_post_commit_dispatcher.py::test_crash_between_publish_and_mark_is_redelivered_by_the_relay` |
| G-OUTBOX-08 | On the request / consumer / contract paths (wait=False) post-commit dispatch is a hand-off to a background coalescer that does not wait for the broker; db\_session()/Celery/scripts (wait=True) publish in line; queue overflow falls back to the relay. | `sandloom/core/tests/test_post_commit_dispatcher.py::test_request_path_does_not_wait_for_the_broker`<br>`sandloom/core/tests/test_post_commit_dispatcher.py::test_db_session_path_publishes_and_marks_in_line`<br>`sandloom/core/tests/test_post_commit_dispatcher.py::test_queue_overflow_drops_to_the_relay`<br>`sandloom/core/tests/test_post_commit_dispatcher.py::test_consumer_session_hands_its_events_to_the_coalescer` |
| G-OUTBOX-09 | One UoW's own post-commit dispatch is sequential in emission order. | `sandloom/core/tests/test_uow_outbox.py::test_post_commit_dispatch_preserves_unit_of_work_order`<br>`sandloom/core/tests/test_pipelined_dispatch.py::test_queued_uows_go_out_in_one_ordered_batch` |
| G-OUTBOX-10 | The relay retries a failed row with capped per-row exponential backoff, and a row exhausting OUTBOX\_RELAY\_MAX\_ATTEMPTS is dead-lettered (dead\_at set, outbox\_dead\_total) instead of retried forever; a dead row can be requeued to pending. | `sandloom/core/tests/test_outbox_relay_backoff.py::test_failed_publish_defers_retry_with_backoff`<br>`sandloom/core/tests/test_outbox_relay_backoff.py::test_relay_backoff_grows_and_is_capped`<br>`sandloom/core/tests/test_outbox_relay_batch.py::test_failures_back_off_per_row_and_dead_letter`<br>`sandloom/core/tests/test_outbox_relay_backoff.py::test_requeue_dead_returns_rows_to_pending` |
| G-OUTBOX-11 | A broker outage burns no relay attempts: it trips a cool-down circuit (OUTBOX\_RELAY\_CIRCUIT\_\*) that skips relay ticks, so an outage never dead-letters healthy rows. | `sandloom/core/tests/test_outbox_relay_backoff.py::test_broker_outage_burns_no_attempts_and_stops_batch`<br>`sandloom/core/tests/test_outbox_relay_backoff.py::test_open_circuit_skips_relay_ticks`<br>`sandloom/core/tests/test_relay_batch_publish.py::test_relay_batch_outage_counts_no_attempt_and_trips_circuit`<br>`sandloom/core/tests/test_batch_publish.py::test_redis_availability_replies_do_not_burn_relay_attempts` |
| G-OUTBOX-12 | A poison row in a relay batch is isolated (per-row fallback) and still dead-lettered, while the other rows are delivered. | `sandloom/core/tests/test_relay_batch_publish.py::test_relay_batch_poison_falls_back_per_row`<br>`sandloom/core/tests/test_relay_batch_publish.py::test_relay_poison_row_still_dead_letters`<br>`sandloom/core/tests/test_kernel_hardening.py::test_outbox_relay_dead_letters_poison_message`<br>`sandloom/core/tests/test_kernel_hardening.py::test_relay_processes_each_message_independently` |
| G-OUTBOX-13 | Concurrent relays and post-commit marks settle every row exactly once: relays partition the backlog by lease, a settle never touches a reclaimed lease, and a late mark never resurrects a dead-lettered row. | `sandloom/core/tests/test_outbox_relay_batch.py::test_concurrent_relays_partition_the_backlog`<br>`sandloom/core/tests/test_outbox_relay_batch.py::test_settle_never_touches_a_reclaimed_lease`<br>`sandloom/core/tests/test_outbox_relay_batch.py::test_post_commit_mark_never_resurrects_a_dead_letter`<br>`sandloom/core/tests/test_outbox_relay_batch.py::test_concurrent_relays_and_marks_settle_every_row_once` |
| G-OUTBOX-14 | The relay retention purge deletes only delivered outbox rows older than OUTBOX\_RETENTION\_HOURS (never pending or dead rows), on a time cadence within a time budget. | `sandloom/core/tests/test_outbox_relay_batch.py::test_retention_purge_deletes_only_old_delivered_rows`<br>`sandloom/core/tests/test_outbox_relay_purge_cadence.py::test_purge_runs_on_a_time_cadence_not_a_tick_count`<br>`sandloom/core/tests/test_outbox_relay_purge_cadence.py::test_purge_stops_at_its_time_budget` |
| G-OUTBOX-15 | Events carry NO ordering guarantee, not even within one channel or aggregate; consumers order by a per-aggregate version applied with version guards, so redelivered or reordered events never roll state back. (The relay holding a channel's later rows behind an earlier row waiting to retry is an implementation detail, not a guarantee: one poison row delays its channel until it is dead-lettered.) | `sandloom/core/tests/test_projections.py::test_consumer_applies_and_is_monotonic_and_idempotent`<br>`sandloom/core/tests/test_projection_upsert_guards.py::test_null_stored_version_is_updatable` |
| G-OUTBOX-16 | OUTBOX\_RELAY\_ENABLED=off in a production app process is refused unless the accepted risk relay\_disabled is declared, and under gunicorn exactly one elected worker runs the relay (others delegated). | `sandloom/core/tests/test_accepted_risks.py::test_relay_off_in_prod_is_refused_unless_accepted`<br>`sandloom/core/tests/test_accepted_risks.py::test_delegated_relay_needs_a_gunicorn_worker`<br>`sandloom/core/tests/test_gunicorn_conf.py::test_gunicorn_elects_exactly_one_relay_leader` |

### G-EFFECT

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-EFFECT-01 | on\_commit callbacks run only after the UoW commits, never on rollback, never twice; a failing callback is logged and never fails the committed request; outside a UoW the callback runs at once. | `sandloom/core/tests/test_on_commit.py::test_runs_after_commit_only`<br>`sandloom/core/tests/test_on_commit.py::test_rollback_never_runs`<br>`sandloom/core/tests/test_on_commit.py::test_failing_callback_does_not_fail_commit`<br>`sandloom/core/tests/test_on_commit.py::test_outside_uow_runs_immediately` |
| G-EFFECT-02 | Post-commit callbacks run under the tenant bound when they were registered, not the tenant bound at commit time. | `sandloom/core/tests/test_open_uow.py::test_post_commit_callbacks_see_the_tenant_bound_at_registration` |
| G-EFFECT-03 | before\_commit checks run after the handler, in registration order, right before COMMIT; the first failing check rolls the UoW back and propagates; outside a UoW the check runs at once; the testing run\_on\_commit seam runs before\_commit checks first. | `sandloom/core/tests/test_before_commit.py::test_checks_run_in_order_after_the_handler_and_before_commit`<br>`sandloom/core/tests/test_before_commit.py::test_a_failing_check_rolls_back_instead_of_committing`<br>`sandloom/core/tests/test_before_commit.py::test_outside_a_unit_of_work_the_check_runs_at_once`<br>`sandloom/core/tests/test_before_commit.py::test_testing_run_on_commit_runs_the_before_commit_checks_first`<br>**gap (S)**: No test proves that a before\_commit check registered inside a rolled-back savepoint is skipped (ADR §3 stage 1). |
| G-EFFECT-04 | All durable dedupe stores (consumer\_inbox, contract\_command\_log, http\_idempotency\_log) are adapters of one relational claim implementation (sandloom.core.sql\_alchemy.dedupe\_log): a claim is decided by the in-flight transaction, a rolled-back claim leaves no record, the claim never flushes the caller's pending objects, and the bounded purge removes only due records. | `sandloom/core/tests/test_dedupe_record_adapters.py::test_every_store_is_an_adapter_of_one_dedupe_log`<br>`sandloom/core/tests/test_dedupe_record_adapters.py::test_rolled_back_claim_leaves_no_record`<br>`sandloom/core/tests/test_dedupe_record_adapters.py::test_concurrent_claim_is_decided_by_the_in_flight_transaction`<br>`sandloom/core/tests/test_dedupe_record_adapters.py::test_claim_never_flushes_the_callers_pending_objects` |
| G-EFFECT-05 | Every replay record shares one set of bounds from sandloom.core.effects / replay\_limits: replayed body/result at most 256 KiB (larger -\> non-replayable marker, never re-run) and a 24 h default TTL; argument digests are canonical. | `sandloom/core/tests/test_dedupe_record_adapters.py::test_tag_and_ttl_and_replay_bounds`<br>`sandloom/core/tests/test_dedupe_record_adapters.py::test_replay_outcome_caps_and_refuses_unencodable`<br>`sandloom/core/tests/test_dedupe_record_adapters.py::test_args_digest_is_canonical_with_a_repr_fallback`<br>`sandloom/core/tests/test_replay_limits.py::test_replay_body_caps_share_one_bound` |

### G-INBOX

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-INBOX-01 | durable\_inbox(consumer).claim(db, event\_id) inserts with ON CONFLICT on UNIQUE(consumer, event\_id) in the handler's own UoW: the first claim returns True, a duplicate returns False, and a handler's own constraint violation is never read as a duplicate. | `sandloom/core/tests/test_durable_inbox_claim.py::test_duplicate_claim_is_skipped`<br>`sandloom/core/tests/test_durable_inbox_claim.py::test_handler_constraint_violation_is_not_read_as_duplicate`<br>`sandloom/core/tests/test_kernel_hardening.py::test_durable_inbox_claims_once`<br>`sandloom/core/tests/test_dedupe_record_adapters.py::test_claimed_once_then_duplicate` |
| G-INBOX-02 | A durable=True consumer dedupes redeliveries by the DB claim on the envelope id and never touches the cache inbox. | `sandloom/core/tests/test_streams_event_bus.py::test_durable_consumer_dedupes_redelivery_without_cache_inbox`<br>`sandloom/core/tests/test_event_consumers.py::test_consumer_callback_advertises_durable_dedupe` |
| G-INBOX-03 | The cache-backed consumer inbox (durable=False) dedupes on the event id best-effort and fails open on a cache error (the effect stays at-least-once). | `sandloom/core/tests/test_transport.py::test_consumer_inbox_dedupes_on_event_id`<br>`sandloom/core/tests/test_transport.py::test_consumer_inbox_fails_open_on_cache_error` |
| G-INBOX-04 | Inbox purge removes old claims in bounded batches, each in its own UoW, backed by a declared purge index. | `sandloom/core/tests/test_durable_inbox_claim.py::test_purge_with_limit_deletes_old_claims_in_bounded_batches`<br>`sandloom/core/tests/test_durable_inbox_claim.py::test_inbox_purge_cli_loops_batches_in_separate_units_of_work`<br>`sandloom/core/tests/test_durable_inbox_claim.py::test_consumer_inbox_declares_the_purge_index` |

### G-CMD

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-CMD-01 | A command contract's dedupe record is claimed in the provider's own UoW and commits iff the effect commits, so a retry after a crash following COMMIT replays the stored result and never re-executes. | `tests/integration/test_durable_command_idempotency.py::test_crash_after_commit_replays_and_never_reexecutes`<br>`tests/integration/test_durable_command_idempotency.py::test_billing_credit_spend_survives_crash_and_retry` |
| G-CMD-02 | Concurrent duplicates of a command execute exactly once; a duplicate that outwaits CONTRACT\_DB\_LOCK\_TIMEOUT\_MS gets 503 ContractCallInProgress. | `tests/integration/test_durable_command_idempotency.py::test_concurrent_duplicates_execute_exactly_once`<br>`tests/integration/test_durable_command_idempotency.py::test_duplicate_outwaiting_the_lock_timeout_is_in_progress`<br>`sandloom/core/tests/test_contract_parity.py::test_rpc_in_progress_maps_to_a_clear_unavailable` |
| G-CMD-03 | Reusing a command idempotency key with other arguments raises ContractIdempotencyConflict (409), locally and over RPC. | `tests/integration/test_durable_command_idempotency.py::test_same_key_other_arguments_conflicts_locally_and_over_rpc`<br>`sandloom/core/tests/test_service_rpc.py::test_command_contract_is_deduped_by_the_framework_locally_and_over_rpc` |
| G-CMD-04 | A failed command leaves no dedupe record (the retry runs afresh), and a provider killed mid-command holds no reservation (its uncommitted claim dies with the connection, no TTL stall). This includes an earlier attempt of the same call still in flight (attempt 1 unanswered, attempt 2 answered with a failure, attempt 1 then runs afresh and may succeed), the semantics of a client retry after a failure; the unanswered attempt keeps the caller's request in doubt (G-UNIT-02). | `tests/integration/test_durable_command_idempotency.py::test_failed_command_leaves_no_record_and_retry_executes`<br>`tests/integration/test_durable_command_idempotency.py::test_provider_killed_mid_command_does_not_block_the_retry` |
| G-CMD-05 | Local and RPC calls of a command share one dedupe namespace/record (a remote retry of a local call replays). | `tests/integration/test_durable_command_idempotency.py::test_local_and_rpc_share_one_record`<br>`sandloom/core/tests/test_contract_runtime.py::test_direct_command_call_shares_the_registered_dedupe_namespace` |
| G-CMD-06 | A command result larger than 256 KiB is stored as a non-replayable marker: a repeat raises ContractResultNotReplayable (409) and the method is never re-run. | `tests/integration/test_durable_command_idempotency.py::test_oversized_result_is_never_reexecuted`<br>`sandloom/core/tests/test_contract_parity.py::test_command_dedupe_is_shared_bounded_and_size_capped` |
| G-CMD-07 | A command record expires after CONTRACT\_COMMAND\_DEDUPE\_TTL\_SECONDS (the key then runs afresh by taking over the expired record), an in-flight claim past its TTL is never taken over, and purge never deletes a concurrently taken-over record. | `tests/integration/test_durable_command_idempotency.py::test_expired_record_is_taken_over_and_purged`<br>`tests/integration/test_durable_command_idempotency.py::test_an_in_flight_claim_past_its_ttl_is_never_taken_over`<br>`tests/integration/test_durable_command_idempotency.py::test_purge_never_deletes_a_record_taken_over_concurrently` |
| G-CMD-08 | A command contract must name its mandatory idempotency\_key argument at registration and commits its own UoW. | `sandloom/core/tests/test_contract_runtime.py::test_command_contract_must_name_its_idempotency_key`<br>`sandloom/core/tests/test_contract_runtime.py::test_command_commits_and_requires_idempotency_key` |
| G-CMD-09 | Without a database command log (no command\_idempotency feature) command dedupe falls back to the cache, fail-closed: a cache outage raises ContractIdempotencyUnavailable (503) and the method does not run; the fallback follows IDEMPOTENCY\_CACHE\_INSTANCE. | `sandloom/core/tests/test_service_rpc.py::test_command_contract_fails_closed_when_the_store_is_down`<br>`sandloom/core/tests/test_idempotency_cache_instance.py::test_contract_command_cache_fallback_follows_the_setting` |

### G-IDEM

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-IDEM-01 | Cache-form idempotency\_guard replays a successful response (same status/body, Idempotent-Replay: true) without re-running the endpoint. | `sandloom/core/tests/test_api_robustness.py::test_idempotency_replays_successful_response`<br>`sandloom/core/tests/test_idempotency_record_format.py::test_json_body_is_stored_raw_and_replays_byte_identical` |
| G-IDEM-02 | A retry while the first request is pending gets 409; the pending reservation has a short TTL that is renewed while the request runs and self-heals if the request dies. | `sandloom/core/tests/test_api_robustness.py::test_idempotency_pending_request_conflicts`<br>`sandloom/core/tests/test_api_robustness.py::test_idempotency_pending_reservation_uses_short_ttl`<br>`sandloom/core/tests/test_idempotency_pending_renewal.py::test_a_running_request_keeps_its_reservation_past_the_pending_ttl`<br>`sandloom/core/tests/test_idempotency_pending_renewal.py::test_a_dead_requests_reservation_still_self_heals` |
| G-IDEM-03 | After a failed attempt (error response or unhandled exception) the key is released so a retry reserves afresh; the error response is sent before the release. | `sandloom/core/tests/test_api_robustness.py::test_idempotency_failed_attempt_allows_retry`<br>`sandloom/core/tests/test_api_robustness.py::test_idempotency_unhandled_exception_releases_key`<br>`sandloom/core/tests/test_security_review_regressions.py::test_error_response_is_sent_before_cache_release` |
| G-IDEM-04 | Reusing an Idempotency-Key with a different request body gets 422 (cache and durable forms); a missing key is rejected. | `sandloom/core/tests/test_api_robustness.py::test_idempotency_rejects_key_reused_with_different_body`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_same_key_other_request_is_422`<br>`sandloom/core/tests/test_api_robustness.py::test_idempotency_guard_requires_key` |
| G-IDEM-05 | Every cache-guard transition is a compare-and-set on the reserving request's owner token: an expired owner can neither release nor finalise over a retry's reservation. | `sandloom/core/tests/test_idempotency_cas.py::test_redis_cas_primitives`<br>`sandloom/core/tests/test_idempotency_cas.py::test_expired_owner_cannot_release_a_retrys_reservation`<br>`sandloom/core/tests/test_idempotency_cas.py::test_expired_owner_cannot_finalise_over_a_retrys_reservation` |
| G-IDEM-06 | Idempotency keys are scoped per caller and tenant (never replayed across tenants), and an anonymous caller never gets a stored body replayed (status only). | `sandloom/core/tests/test_api_robustness.py::test_idempotency_never_replays_across_tenants`<br>`sandloom/core/tests/test_api_robustness.py::test_idempotency_anonymous_retry_is_deduped_without_body_replay`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_key_is_scoped_to_caller_and_tenant`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_anonymous_caller_gets_status_only` |
| G-IDEM-07 | A response larger than the capture cap is returned in full but stored as a non-replayable success marker (never re-executed, body not preserved); a corrupt record is discarded instead of blocking the key. | `sandloom/core/tests/test_api_robustness.py::test_idempotency_large_response_returns_full_body_stores_marker`<br>`sandloom/core/tests/test_idempotency_record_format.py::test_an_old_reader_replays_a_new_record_status_only_never_re_executes`<br>`sandloom/core/tests/test_api_robustness.py::test_idempotency_corrupt_record_does_not_block_key` |
| G-IDEM-08 | With fail\_closed=True a cache outage answers 503 (never a possible duplicate); the default fail-open guard lets the request through. | `sandloom/core/tests/test_api_robustness.py::test_idempotency_fail_closed_returns_503_on_cache_outage`<br>`sandloom/core/tests/test_api_robustness.py::test_idempotency_fail_open_allows_on_cache_outage`<br>`sandloom/core/tests/test_idempotency_cache_instance.py::test_fail_closed_follows_the_idempotency_instance` |
| G-IDEM-09 | idempotency\_guard(durable=True) claims its record in the request's DbSession UoW and commits it with the response: a retry replays the stored response and the effect runs once; a rolled-back request leaves no record; a returned 4xx is replayed; a returned 5xx is never replayed. | `sandloom/core/tests/test_durable_idempotency_guard.py::test_replay_returns_the_stored_response_and_runs_once`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_rolled_back_request_leaves_no_record`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_returned_4xx_is_the_committed_outcome_and_replays`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_returned_5xx_is_never_replayed` |
| G-IDEM-10 | An in-flight duplicate of a durable-guarded request gets 409 at once (advisory try-lock), never a wait on the first request's uncommitted claim. | `sandloom/core/tests/test_durable_idempotency_guard.py::test_concurrent_in_flight_duplicate_is_409_at_once` |
| G-IDEM-11 | After an ambiguous COMMIT a durable-guarded retry replays if the transaction committed and runs afresh if it did not — never twice. | `sandloom/core/tests/test_durable_idempotency_guard.py::test_ambiguous_commit_that_committed_is_replayed_not_rerun`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_ambiguous_commit_that_did_not_commit_runs_afresh` |
| G-IDEM-12 | Durable guard records expire after the guard TTL (taken over then purged); oversized/streamed responses replay status only; a route whose response was not captured is refused rather than re-run; routes added after create\_app are captured too. | `sandloom/core/tests/test_durable_idempotency_guard.py::test_expired_record_is_taken_over_and_purged`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_unreplayable_response_replays_status_only`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_route_without_capture_is_refused_not_rerun`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_route_added_after_create_app_is_captured` |
| G-IDEM-14 | create\_app refuses (GuardOrderError) a route where FastAPI resolves the idempotency guard before its authenticating dependency. | `sandloom/core/tests/test_guard_order.py::test_guard_in_route_dependencies_before_param_auth_is_refused`<br>`sandloom/core/tests/test_guard_order.py::test_guard_param_before_auth_param_is_refused`<br>`sandloom/core/tests/test_guard_order.py::test_custom_authenticator_counts_once_marked`<br>`sandloom/core/tests/test_guard_order.py::test_bundled_app_routes_pass` |
| G-IDEM-15 | A valid token's revocation and block state is checked live before an idempotency reservation or replay, even when the guard precedes the endpoint's auth dependency. | `sandloom/core/tests/test_security_review_regressions.py::test_replay_checks_live_auth_before_cache_lookup` |
| G-IDEM-16 | Cache-guard records live in SECURITY\_CACHE\_INSTANCE unless IDEMPOTENCY\_CACHE\_INSTANCE moves them (with task-enqueue dedupe and the command cache fallback); revocation marks never move, and the strict profile refuses an idempotency instance that is not its own durable Redis. | `sandloom/core/tests/test_idempotency_cache_instance.py::test_default_keeps_guard_records_in_the_security_cache`<br>`sandloom/core/tests/test_idempotency_cache_instance.py::test_reserve_replay_conflict_release_use_the_idempotency_instance`<br>`sandloom/core/tests/test_idempotency_cache_instance.py::test_revocation_marks_stay_in_the_security_cache`<br>`sandloom/core/tests/test_idempotency_cache_instance.py::test_strict_profile_refuses_an_unsafe_idempotency_instance` |
| G-IDEM-17 | The automatic guard (user writes carrying Idempotency-Key, with no explicit guard, no\_idempotency or operation intent) always replays the status only, for a DbSession write and for an ordinary write through db() (the lazy request unit holds the durable record) (mode S: Idempotent-Replay: true, empty body, the handler never runs again), including after an ownership transfer; replay\_safe(reason=) opts a non-sensitive route in to body replay (mode B); a sensitive write is always S; the replay is decided after every route-level dependency (session check, gates, step-up); a different request under the same key is 422, and cross-route or cross-tenant reuse never replays. | `tests/integration/test_automatic_idempotency.py::test_the_automatic_guard_replays_the_status_only_never_a_body`<br>`tests/integration/test_automatic_idempotency.py::test_replay_safe_replays_the_body_after_every_route_dependency`<br>`tests/integration/test_automatic_idempotency.py::test_a_revoked_session_is_refused_before_any_replay`<br>`tests/integration/test_automatic_idempotency.py::test_mode_x_re_runs_authorization_and_the_handler`<br>`tests/integration/test_automatic_idempotency.py::test_a_lazy_db_write_gets_the_durable_status_replay` |
| G-IDEM-18 | A write route without a request unit, or whose request unit's database keeps no durable request log, gets the fail-closed in-flight form of the automatic guard: a concurrent duplicate is 409, a cache outage 503, and nothing is stored. | `tests/integration/test_automatic_idempotency.py::test_without_a_request_unit_the_guard_is_the_fail_closed_in_flight_form` |

### G-TASK

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-TASK-01 | enqueue(..., idempotency\_key=) publishes one real message for successful retries (task id replayed within dedupe\_ttl\_seconds), including parallel retries; the replay window expires after the TTL. | `sandloom/core/tests/test_enqueue_deduplication.py::test_successful_retries_publish_one_real_message`<br>`sandloom/core/tests/test_enqueue_deduplication.py::test_parallel_retries_never_publish_twice`<br>`sandloom/core/tests/test_enqueue_deduplication.py::test_replay_window_expires_on_real_redis`<br>`sandloom/core/tests/test_enqueue_deduplication.py::test_lost_finalize_ack_keeps_the_original_id_on_retry` |
| G-TASK-02 | Reusing an enqueue idempotency key with other arguments fails with 409; keys are tenant-scoped and the payload is snapshotted before reserving. | `sandloom/core/tests/test_enqueue_deduplication.py::test_key_cannot_be_reused_for_other_arguments`<br>`sandloom/core/tests/test_enqueue_deduplication.py::test_tenants_do_not_share_enqueue_keys`<br>`sandloom/core/tests/test_enqueue_deduplication.py::test_payload_is_snapshotted_before_reserving` |
| G-TASK-03 | Enqueue dedupe fails closed: an unavailable dedupe store prevents the publish (503), and an ambiguous broker send keeps the key pending (503 task\_enqueue\_uncertain, never republished) until reconciled; deterministic send/serialisation errors release the reservation. | `sandloom/core/tests/test_enqueue_deduplication.py::test_unavailable_store_prevents_publish`<br>`sandloom/core/tests/test_enqueue_deduplication.py::test_uncertain_broker_ack_is_not_republished`<br>`sandloom/core/tests/test_enqueue_deduplication.py::test_ambiguous_send_error_still_keeps_the_key_pending`<br>`sandloom/core/tests/test_enqueue_deduplication.py::test_deterministic_send_error_releases_the_reservation` |
| G-TASK-04 | run\_async runs task coroutines on one persistent event loop per worker process (fresh after fork, engines disposed on it at shutdown) and refuses to run inside a running loop. | `sandloom/core/tests/test_async_runner.py::test_sequential_calls_share_one_persistent_loop`<br>`sandloom/core/tests/test_async_runner.py::test_inherited_loop_is_replaced_after_fork`<br>`sandloom/core/tests/test_async_runner.py::test_run_async_inside_a_running_loop_is_rejected`<br>`sandloom/core/tests/test_async_runner.py::test_shutdown_runs_cleanups_on_the_worker_loop_then_closes_it` |
| G-TASK-05 | Celery tasks store no result by default (task\_ignore\_result, also on driver-built apps, enqueue subscribes no result consumer), the worker prefetches one message, and acks\_late stays a per-task opt-in. | `sandloom/core/tests/test_cache_batch_and_celery_defaults.py::test_worker_reliability_defaults`<br>`sandloom/core/tests/test_cache_batch_and_celery_defaults.py::test_driver_built_apps_ignore_results_by_default`<br>`sandloom/core/tests/test_cache_batch_and_celery_defaults.py::test_enqueue_never_subscribes_a_result_consumer` |

### G-WINDOW

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-WINDOW-01 | guarantee\_window\_problems reports command\_dedupe\_below\_retry\_horizon when CONTRACT\_COMMAND\_DEDUPE\_TTL\_SECONDS is below the relay redelivery horizon (OUTBOX\_RELAY\_MAX\_ATTEMPTS x capped backoff); defaults satisfy every window. | `sandloom/core/tests/test_guarantee_windows.py::test_defaults_hold_every_window`<br>`sandloom/core/tests/test_guarantee_windows.py::test_relay_horizon_is_the_capped_backoff_sum`<br>`sandloom/core/tests/test_guarantee_windows.py::test_command_ttl_below_the_relay_horizon` |
| G-WINDOW-02 | Production refuses to start on a violated guarantee window (GuaranteeWindowError naming the risk id), other environments warn, and a window is waived only by an accepted risk registered in accepted\_risks. | `sandloom/core/tests/test_guarantee_windows.py::test_production_refuses_and_names_the_risk`<br>`sandloom/core/tests/test_guarantee_windows.py::test_dev_warns`<br>`sandloom/core/tests/test_guarantee_windows.py::test_accepted_risks_are_not_refused`<br>`sandloom/core/tests/test_guarantee_windows.py::test_create_app_refuses_in_production` |
| G-WINDOW-03 | sandloom outbox purge --inbox-days D refuses D \< EVENT\_STREAM\_RETENTION\_SECONDS + 1 day (any D when retention is unbounded) unless --force (risk inbox\_purge\_below\_redelivery). | `sandloom/core/tests/test_guarantee_windows.py::test_inbox_purge_window`<br>`sandloom/core/tests/test_durable_inbox_claim.py::test_inbox_purge_inside_the_redelivery_window_is_refused`<br>`sandloom/core/tests/test_guarantee_windows.py::test_window_risk_ids_are_registered_accepted_risks` |

### G-EVENT

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-EVENT-01 | A consumer registered by a module gets the durable group :: equally named consumers of two modules (or of one module in two services) get distinct groups and each receives every event, while replicas of one service share the group. | `sandloom/core/tests/test_event_consumers.py::test_same_consumer_name_in_two_modules_gets_two_groups`<br>`sandloom/core/tests/test_streams_split_identity.py::test_same_name_in_two_services_each_receive_every_event`<br>`sandloom/core/tests/test_streams_split_identity.py::test_replicas_of_one_service_share_the_group` |
| G-EVENT-02 | Startup fails when two consumers resolve to one group in a process, when an explicit group= is reused by another module, or when the group is already owned by another module / database (sandloom:group-owner fingerprint); consumer names are unique across event and projection consumers. | `sandloom/core/tests/test_streams_split_identity.py::test_duplicate_group_in_one_process_fails_startup`<br>`sandloom/core/tests/test_streams_split_identity.py::test_explicit_group_reused_by_other_module_fails_startup`<br>`sandloom/core/tests/test_streams_split_identity.py::test_group_owned_by_other_database_fails_startup`<br>`sandloom/core/tests/test_event_consumers.py::test_consumer_name_unique_across_event_and_projection` |
| G-EVENT-03 | Required consumers (the default) abort startup when they cannot connect (all consumers are still attempted, transient failures retried within CONSUMER\_STARTUP\_TIMEOUT\_SECONDS), and /ready fails while a required live subscription is lost; required=False consumers never delay startup or readiness. | `sandloom/core/tests/test_consumer_runtime.py::test_lifespan_aborts_when_required_consumer_cannot_start`<br>`sandloom/core/tests/test_consumer_runtime.py::test_ready_endpoint_rejects_unavailable_required_consumer`<br>`sandloom/core/tests/test_consumer_runtime.py::test_required_runtime_readiness_tracks_live_subscription_state`<br>`sandloom/core/tests/test_consumer_runtime.py::test_optional_consumer_never_delays_application_startup` |
| G-EVENT-04 | A consumer subscription whose task dies is restarted by the supervised runtime, and a permanent subscription contract error is not retried forever. | `sandloom/core/tests/test_consumer_runtime.py::test_runtime_restarts_a_subscription_whose_task_dies`<br>`sandloom/core/tests/test_consumer_runtime.py::test_permanent_subscription_contract_error_is_not_retried_forever`<br>`sandloom/core/tests/test_consumer_runtime.py::test_required_consumer_retries_transient_startup_failure` |
| G-EVENT-05 | Each consumer delivery runs async (session, envelope) in a UoW: commit on success; a handler error leaves the delivery for redelivery (rollback). | `sandloom/core/tests/test_event_consumers.py::test_start_event_consumers_wires_and_delivers`<br>`sandloom/core/tests/test_streams_event_bus.py::test_failed_handler_leaves_message_for_redelivery`<br>`sandloom/core/tests/test_inmemory_bus_delivery.py::test_failing_consumer_is_retried_until_success`<br>**gap (S)**: No test asserts the handler's DB writes are rolled back when the handler raises (the consumer UoW rollback itself); tests check redelivery with fake sessions only. |
| G-EVENT-06 | A consumer with dead\_letter=False retries a failing delivery forever with capped backoff instead of dead-lettering it, while default consumers are dead-lettered after the bus's delivery budget. | `sandloom/core/tests/test_revocation_never_dead_lettered.py::test_in_memory_bus_retries_a_never_dead_letter_consumer_forever`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_streams_outage_longer_than_the_old_budget_is_applied_not_dlqd`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_streams_default_consumers_are_still_dead_lettered`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_redis_streams_never_dead_letter_group_survives_the_old_budget` |
| G-EVENT-07 | A handler raising PoisonEventError (public in sandloom.core.di) is dead-lettered at once, even on a dead\_letter=False consumer. | `sandloom/core/tests/test_revocation_never_dead_lettered.py::test_in_memory_bus_dead_letters_poison_immediately`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_streams_malformed_revocation_is_dead_lettered_at_once`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_poison_error_is_public` |
| G-EVENT-08 | bind\_tenant\_from="" binds the payload's tenant for the handler; consumers otherwise never inherit the publisher's tenant context. | `sandloom/core/tests/test_event_consumers.py::test_bind_tenant_from_binds_the_envelope_tenant`<br>`sandloom/core/tests/test_inmemory_bus_delivery.py::test_consumer_does_not_inherit_publisher_tenant` |
| G-EVENT-09 | start\_from (earliest \| latest \| timestamp) picks where a NEW group starts and is validated together with group=. | `sandloom/core/tests/test_event_consumers.py::test_start_from_and_group_are_validated`<br>`sandloom/core/tests/test_streams_split_identity.py::test_start_from_latest_skips_history`<br>`sandloom/core/tests/test_streams_split_identity.py::test_start_from_timestamp` |
| G-EVENT-10 | The Streams transport redelivers unacked entries, reclaims dead consumers' pending entries via XAUTOCLAIM, dead-letters an entry to .dlq after exactly the max deliveries, and acks the source only after the DLQ append succeeded. | `sandloom/core/tests/test_transport.py::test_unacked_message_is_redelivered`<br>`sandloom/core/tests/test_transport.py::test_reclaim_advances_server_scan_cursor`<br>`sandloom/core/tests/test_transport.py::test_poison_dead_lettered_after_exactly_max_deliveries`<br>`sandloom/core/tests/test_transport.py::test_dlq_append_failure_leaves_source_message_unacked` |
| G-EVENT-11 | A DLQ replay is targeted at the group that dead-lettered the entry; other groups ack-skip it (Streams and memory transports). | `sandloom/core/tests/test_streams_split_identity.py::test_dlq_replay_reaches_only_the_failed_group`<br>`sandloom/core/tests/test_streams_split_identity.py::test_memory_dlq_replay_is_targeted_too`<br>`sandloom/core/tests/test_admin_operations.py::test_stream_dlq_replay_targets_the_group_and_audits_first` |
| G-EVENT-12 | Streams are trimmed by age (EVENT\_STREAM\_RETENTION\_SECONDS) and optionally by length (EVENT\_STREAM\_MAXLEN) on publish. | `sandloom/core/tests/test_streams_split_identity.py::test_publish_trims_entries_older_than_retention`<br>`sandloom/core/tests/test_streams_split_identity.py::test_publish_caps_stream_length` |
| G-EVENT-13 | Re-registering an identical event schema is a no-op, while a breaking change (new required field or removed required field on the same version) fails registration / module load. | `sandloom/core/tests/test_core_event_schemas.py::test_re_register_idempotent`<br>`sandloom/core/tests/test_schema_compat.py::test_add_required_field_same_version_rejected`<br>`sandloom/core/tests/test_schema_compat.py::test_remove_required_field_rejected`<br>`sandloom/core/tests/test_schema_compat.py::test_add_optional_field_allowed` |
| G-EVENT-14 | Durable/required consumers are refused on the volatile Pub/Sub bus unless the volatile\_event\_bus risk is accepted, and the memory/redis Pub/Sub drivers are refused in production without it. | `sandloom/core/tests/test_volatile_bus_guards.py::test_durable_consumer_refused_on_pubsub_bus`<br>`sandloom/core/tests/test_volatile_bus_guards.py::test_memory_bus_allowed_in_dev_refused_in_prod`<br>`sandloom/core/tests/test_volatile_bus_guards.py::test_driver_refuses_volatile_bus_in_prod` |

### G-AUDIT

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-AUDIT-01 | record\_audit writes the audit\_log row in the current UoW, so it commits iff the business change commits. | `sandloom/core/tests/test_audit_and_secrets.py::test_record_audit_writes_in_uow`<br>`sandloom/core/tests/test_revocation_freshness.py::test_block_write_outage_is_503_and_rolls_back_audit`<br>`sandloom/core/tests/test_fused_effect_rows.py::test_outbox_and_audit_rows_are_one_statement`<br>**gap (S)**: No generic test that a plain record\_audit followed by a UoW rollback leaves no audit\_log row; only the block\_user path proves it. |
| G-AUDIT-02 | record\_audit defaults to required=True: outside a UoW, with a non-UUID actor\_id, or without an audit store it raises AuditUnavailableError; required=False is skipped silently (best-effort, marker required in module code). | `sandloom/core/tests/test_audit_and_secrets.py::test_record_audit_outside_uow_raises_by_default`<br>`sandloom/core/tests/test_audit_and_secrets.py::test_record_audit_best_effort_outside_uow_is_skipped`<br>`sandloom/core/tests/test_audit_and_secrets.py::test_record_audit_default_rejects_invalid_actor`<br>`sandloom/core/tests/test_testing_harness.py::test_required_audit_without_uow_raises`<br>rule `best-effort-effects`<br>**gap (S)**: The 'no audit store' branch of AuditUnavailableError (database driver without the audit feature) has no direct raising test (test\_audit\_store\_skips\_unsupported\_backend only checks the store is absent). |

### G-AUTH

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-AUTH-01 | Every framework guard and the single bearer parser (bearer\_claims) reject a JWT whose token\_type is not "access" (e.g. a refresh token or a token without token\_type), so it can never act as an access token. | `tests/integration/test_review_hardening.py::test_guard_rejects_token_without_access_type`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_bearer_claims_is_the_single_access_token_parser`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_idempotency_and_tenant_scope_ignore_non_access_bearers` |
| G-AUTH-02 | An access token without iat or exp is rejected (401 at the guard). | `sandloom/core/tests/test_auth_rotation_revocation.py::test_decode_requires_iat`<br>`tests/integration/test_shared_authz_review.py::test_token_without_iat_is_rejected`<br>`tests/integration/test_review_hardening.py::test_decode_token_requires_exp` |
| G-AUTH-03 | JWT\_ALGORITHM accepts only HS256/HS384/HS512, EdDSA, RS256 or ES256 (none and anything else fail settings validation); each asymmetric algorithm is bound to one key type at boot (wrong key type, RSA \< 2048 bit, wrong EC curve refuse startup) and the HS and asymmetric key families are mutually exclusive. | `sandloom/core/tests/test_asymmetric_jwt.py::test_alg_none_is_rejected`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_key_type_is_bound_to_the_algorithm`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_short_rsa_keys_are_refused`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_hmac_material_is_refused_with_asymmetric_and_vice_versa` |
| G-AUTH-04 | In asymmetric mode verification accepts exactly JWT\_ALGORITHM with the one public key named by the token's kid: a token without kid, with an unknown kid, of another algorithm, HMAC-signed with the public PEM or with JWT\_SECRET is rejected. | `sandloom/core/tests/test_asymmetric_jwt.py::test_token_without_or_with_unknown_kid_is_rejected`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_hs256_token_signed_with_the_public_key_is_rejected`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_hs256_token_signed_with_jwt_secret_is_rejected_in_asymmetric_mode`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_token_of_another_asymmetric_algorithm_is_rejected` |
| G-AUTH-05 | Startup role check (critical, the process refuses to serve): an issuer (a process loading a module with issues\_tokens=True) without JWT\_PRIVATE\_KEY, or a verify-only process holding JWT\_PRIVATE\_KEY or JWT\_SECRET, is refused; in prod a split process on a shared HS secret is refused unless the accepted risk shared\_hs\_secret\_split is set, and never under multi\_tenant\_strict. | `sandloom/core/tests/test_asymmetric_jwt.py::test_startup_role_check`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_verify_only_process_may_not_hold_a_symmetric_secret`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_prod_split_deployment_refuses_a_shared_hs_secret`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_create_app_refuses_key_material_that_fails_the_boot_check`<br>`sandloom/core/tests/test_production_profile.py::test_startup_role_check_has_no_shared_secret_opt_out_under_the_profile` |
| G-AUTH-06 | A verify-only process (asymmetric mode, no private key) cannot mint access tokens; in the microservices split every identity-minted access token is EdDSA under the issuer's kid and verifies in processes holding only public keys. | `sandloom/core/tests/test_asymmetric_jwt.py::test_verify_only_process_cannot_mint`<br>`tests/split_conformance/test_split_conformance.py::test_identity_issues_asymmetric_tokens` |
| G-AUTH-07 | Symmetric key rotation: secrets in JWT\_PREVIOUS\_SECRETS still verify but never mint, a token whose kid equals JWT\_KID verifies with JWT\_SECRET only, and in prod previous secrets obey the JWT\_SECRET strength rules and may not contain the current secret. | `sandloom/core/tests/test_auth_rotation_revocation.py::test_kid_is_stamped_and_previous_secret_still_verifies`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_current_kid_is_verified_with_current_secret_only`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_prod_previous_secrets_follow_jwt_secret_rules`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_previous_secrets_must_not_contain_current` |
| G-AUTH-08 | Asymmetric key rotation without forced logout: every kid in JWT\_PUBLIC\_KEYS verifies, a retired (removed) kid is rejected at once, and startup refuses a JWT\_PRIVATE\_KEY that is not the private half of JWT\_PUBLIC\_KEYS\[JWT\_KID\]. | `sandloom/core/tests/test_asymmetric_jwt.py::test_rotation_both_kids_verify_and_retired_kid_is_rejected`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_private_key_must_match_its_published_kid`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_kid_bound_to_a_different_key_is_rejected` |
| G-AUTH-09 | When JWT\_ISSUER / JWT\_AUDIENCE are set, tokens carry them and verification rejects a token of another issuer or audience. | `sandloom/core/tests/test_review_fixes_v2.py::test_jwt_issuer_audience_enforced_when_configured` |
| G-AUTH-10 | Prod refuses to start with a weak or placeholder JWT\_SECRET (secrets and tokens must be \>= 32 chars and not placeholders). | `sandloom/core/tests/test_ops_security_hardening.py::test_prod_rejects_weak_or_placeholder_jwt_secret`<br>`sandloom/core/tests/test_ops_security_hardening.py::test_prod_accepts_random_jwt_secret` |
| G-AUTH-11 | A claims contributor (core.add\_claims\_contributor) runs in its module's own UoW and is auto-exposed as .claims.v1; a failing critical contributor, local or remote, aborts minting with ClaimsUnavailable (503), the first critical failure in registration order winning. | `sandloom/core/tests/test_contract_runtime.py::test_local_contributor_runs_in_its_own_uow_and_is_exposed`<br>`sandloom/core/tests/test_contract_runtime.py::test_critical_local_contributor_failure_is_503`<br>`sandloom/core/tests/test_contract_runtime.py::test_critical_remote_contributor_failure_is_503`<br>`sandloom/core/tests/test_contract_runtime.py::test_first_critical_failure_in_registration_order_wins` |
| G-AUTH-12 | Every minted token carries claims\_sources / claims\_sources\_failed; a module's claim is trusted only when that module is in claims\_sources and not in claims\_sources\_failed, otherwise the fact is unknown and escalates to a live check. | `sandloom/core/tests/test_ops_security_hardening.py::test_claims_provenance_always_stamped`<br>`tests/integration/test_claims_provenance_and_subscribed.py::test_authoritative_provenance_trusts_only_listed_owner`<br>`tests/integration/test_claims_provenance_and_subscribed.py::test_owner_in_both_lists_is_not_trusted`<br>`tests/integration/test_claims_provenance_and_subscribed.py::test_subscribed_escalates_to_live_when_billing_failed_at_mint` |
| G-AUTH-13 | Claims contributors cannot override the identity claims (sub, email, email\_verified, token\_type). | `sandloom_identity/tests/test_token_claims_and_revocation.py::test_unverified_user_and_contributors_cannot_override_identity` |
| G-AUTH-14 | Identity stamps auth\_time (epoch seconds of the primary sign-in) and carries it unchanged through refresh; recency (step-up) checks read auth\_time, never iat, so a refreshed token with a fresh iat is not 'recent'. | `sandloom_identity/tests/services/test_reauth_auth_time.py::test_fresh_iat_with_old_auth_time_is_not_recent`<br>`sandloom_identity/tests/services/test_reauth_auth_time.py::test_refresh_carries_auth_time_unchanged` |
| G-AUTH-15 | Password hashing is argon2id from PASSWORD\_HASH\_PROFILE (strong default m=65536/t=3/p=4, owasp\_min m=19456/t=2/p=1, custom requiring every value); explicit PASSWORD\_HASH\_\* values override the preset and the configured hasher follows settings. | `sandloom/core/tests/test_password_hash_config.py::test_defaults_are_unchanged`<br>`sandloom/core/tests/test_password_hash_config.py::test_owasp_min_preset`<br>`sandloom/core/tests/test_password_hash_config.py::test_explicit_values_override_the_preset`<br>`sandloom/core/tests/test_password_hash_config.py::test_custom_requires_every_value` |
| G-AUTH-16 | Production refuses password-hash parameters below the OWASP argon2id minimum unless the accepted risk weak\_password\_hash is set (outside prod only a warning). | `sandloom/core/tests/test_password_hash_config.py::test_prod_refuses_weak_parameters_with_a_fix`<br>`sandloom/core/tests/test_password_hash_config.py::test_prod_accepts_presets_and_the_explicit_opt_out`<br>`sandloom/core/tests/test_password_hash_config.py::test_every_owasp_equivalent_meets_the_floor` |
| G-AUTH-17 | A successful login re-hashes a hash with other parameters (upgrade or downgrade) in the same UoW; a failed login never re-hashes and a re-hash failure never blocks the login (counted). | `sandloom_identity/tests/services/test_password_rehash_on_login.py::test_login_upgrades_a_weaker_hash`<br>`sandloom_identity/tests/services/test_password_rehash_on_login.py::test_failed_login_never_rehashes`<br>`sandloom_identity/tests/services/test_password_rehash_on_login.py::test_rehash_failure_does_not_block_login`<br>`sandloom/core/tests/test_password_hash_config.py::test_rehash_failure_never_raises_and_is_counted` |
| G-AUTH-18 | A failed login does the same work (one argon2 pool submission against a startup-warmed dummy hash, same cache/SQL steps) whether or not the e-mail has an account, so neither response nor timing reveals account existence. | `sandloom_identity/tests/services/test_login_timing_equalizer.py::test_wrong_password_and_unknown_email_do_identical_work`<br>`sandloom_identity/tests/services/test_login_timing_equalizer.py::test_unknown_email_submits_to_the_password_pool_once`<br>`sandloom_identity/tests/services/test_login_timing_equalizer.py::test_identity_warms_the_dummy_hash_at_startup` |
| G-AUTH-19 | TOTP secrets are encrypted at rest with TWOFA\_ENCRYPTION\_KEY (comma list, newest first, for rotation); in prod with identity loaded the key is mandatory and must be strong (critical startup refusal), only outside prod an unset key derives from JWT\_SECRET. | `sandloom_identity/tests/test_token_claims_and_revocation.py::test_twofa_key_is_mandatory_in_prod`<br>`sandloom_identity/tests/test_module_registration.py::test_missing_twofa_key_registers_a_critical_refusal`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_prod_twofa_key_must_be_strong`<br>`sandloom_identity/tests/services/test_twofa_service.py::TestSecretCrypto::test_key_rotation` |

### G-REVOKE

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-REVOKE-01 | An access token is revoked iff its issue instant iat\_us (microseconds) is \<= its subject's revocation mark; a token without iat\_us is revoked by any mark; every framework guard rejects a revoked token with 401 'Token has been revoked'. | `sandloom/core/tests/test_revocation_windows.py::test_token_issued_earlier_in_the_same_second_is_revoked`<br>`sandloom/core/tests/test_revocation_windows.py::test_a_token_without_iat_us_is_revoked_by_any_mark`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_revoked_token_is_rejected_by_every_framework_guard` |
| G-REVOKE-02 | The mark is written at now + AUTH\_REVOCATION\_CLOCK\_SKEW\_SECONDS, so a token from an issuer whose clock runs ahead is still revoked, while an issuer that ran the issuance check (ensure\_user\_not\_blocked) and tokens minted later in the revoking request are stamped past it. | `sandloom/core/tests/test_revocation_windows.py::test_skew_margin_rejects_tokens_from_an_issuer_clock_running_ahead`<br>`sandloom/core/tests/test_revocation_windows.py::test_issuer_that_saw_the_mark_mints_past_the_margin`<br>`sandloom/core/tests/test_revocation_windows.py::test_tokens_minted_later_in_the_revoking_request_stay_valid` |
| G-REVOKE-03 | The revocation mark is written with the cache's atomic set\_max and never moves backwards; concurrent revocations of one subject all succeed and end at the maximum; a security cache with neither set\_max nor cas is refused for revocation writes. | `sandloom/core/tests/test_auth_rotation_revocation.py::test_revocation_mark_never_moves_backwards`<br>`sandloom/core/tests/test_cache_set_max.py::test_concurrent_revocations_all_succeed_and_keep_the_max`<br>`sandloom/core/tests/test_cache_set_max.py::test_revocation_refuses_a_cache_without_set_max_or_cas`<br>`tests/integration/test_revocation_pipeline.py::test_b_concurrent_revocations_on_real_redis_keep_the_highest_mark` |
| G-REVOKE-04 | revoke\_user\_tokens\_on\_commit writes the mark only after the UoW commits: a rolled-back change never revokes (logs out) the user. | `sandloom_tenancy/tests/test_security_hardening.py::test_rolled_back_member_removal_skips_the_post_commit_revocation`<br>`sandloom_tenancy/tests/test_security_hardening.py::test_rolled_back_org_deletion_skips_the_post_commit_revocation` |
| G-REVOKE-05 | require\_immediate=True writes the mark before COMMIT and, if that write fails, refuses the change with 503 (RevocationUnavailableError) and rolls it back; after commit a second mark max(mark, commit time + skew) revokes tokens minted between the early mark and the commit, and a commit slower than the skew also revokes the request's own tokens (counted auth\_revocation\_remark\_overtook\_floor\_total). | `sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_immediate_revocation_writes_the_mark_before_commit`<br>`sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_immediate_revocation_refuses_the_change_on_a_cache_outage`<br>`sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_immediate_post_commit_remark_revokes_tokens_minted_before_commit`<br>`sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_a_commit_slower_than_the_skew_revokes_the_requests_own_tokens_loudly`<br>`tests/integration/test_revocation_pipeline.py::test_window_immediate_path_is_zero_or_the_change_is_refused` |
| G-REVOKE-06 | Every bundled security event revokes the subject's tokens with require\_immediate=True: tenancy member removal/leave, role change, ownership transfer (both parties), org deletion; identity role revocation, password change/reset, deactivation; block\_user. | `sandloom_tenancy/tests/test_role_change_revocation.py::test_role_change_revokes_the_members_tokens`<br>`sandloom_tenancy/tests/test_role_change_revocation.py::test_ownership_transfer_revokes_both_parties`<br>`sandloom_tenancy/tests/test_security_hardening.py::test_member_removal_and_leave_revoke_the_users_tokens`<br>`sandloom_tenancy/tests/test_security_hardening.py::test_org_deletion_revokes_every_members_tokens`<br>`sandloom_identity/tests/api/test_security_api.py::test_role_revocation_is_immediate_on_identity_endpoints`<br>`tests/integration/test_revocation_pipeline.py::test_a_immediate_removal_needs_no_applier_after_a_crash`<br>**gap (S)**: No test pins that password change/reset and user deactivation use the immediate (pre-commit) path specifically, rather than only that tokens end up revoked. |
| G-REVOKE-07 | Revocation is at-least-once: core.auth.tokens\_revoked.v1 is written to the outbox in the same UoW, the post-commit mark write is retried AUTH\_REVOCATION\_WRITE\_ATTEMPTS times, a still-failing write is counted (auth\_revocation\_deferred\_total) and identity's durable consumer sessions@tokens\_revoked applies it idempotently without ever lowering a newer mark. | `sandloom/core/tests/test_revocation_windows.py::test_revocation_is_persisted_in_the_uow_for_the_durable_applier`<br>`sandloom/core/tests/test_revocation_windows.py::test_exhausted_retries_count_a_deferred_revocation`<br>`sandloom_identity/tests/test_revocation_applier.py::test_applier_never_moves_a_newer_mark_backwards`<br>`tests/integration/test_revocation_pipeline.py::test_a_crash_after_a_failed_mark_write_is_applied_from_the_outbox` |
| G-REVOKE-08 | The revocation applier is registered dead\_letter=False: an outage is retried forever with capped backoff (never parked in the DLQ), only a malformed payload is dead-lettered at once, and while a revocation is stuck /ready answers 503 degraded with /health unaffected. | `sandloom/core/tests/test_revocation_never_dead_lettered.py::test_streams_outage_longer_than_the_old_budget_is_applied_not_dlqd`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_streams_malformed_revocation_is_dead_lettered_at_once`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_ready_degrades_while_a_revocation_is_stuck_liveness_unaffected`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_redis_streams_never_dead_letter_group_survives_the_old_budget` |
| G-REVOKE-09 | The mark lives AUTH\_REVOCATION\_MARK\_TTL\_SECONDS (default max(access TTL + 120 s, 24 h), never shorter than the access lifetime); a token older than the verifier's mark TTL is rejected (fail closed on lifetime drift) and /ready fails on ACCESS\_TOKEN\_EXPIRE\_MINUTES drift between services. | `sandloom/core/tests/test_auth_rotation_revocation.py::test_revocation_mark_outlives_other_services_tokens_by_default`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_revocation_mark_ttl_setting_is_honoured_but_never_too_short`<br>`sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_a_token_older_than_the_mark_ttl_is_rejected_when_no_mark_exists`<br>`sandloom/core/tests/test_contract_compat.py::test_ready_fails_on_access_token_lifetime_drift` |
| G-REVOKE-10 | A subject-state record (sandloom:auth:subject:{id}) that exists but is not a valid value rejects every token of the subject on every authenticated route (one live GET, counted); the issuance check repairs it to now + skew, never deleting or lowering a readable record, and a revocation overwrites a corrupt record. | `sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_corrupt_mark_rejects_every_token_and_is_counted`<br>`sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_corrupt_mark_rejects_on_budgeted_reads_too`<br>`sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_issuance_check_repairs_a_corrupt_mark_so_relogin_works`<br>`sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_a_revocation_overwrites_a_corrupt_mark`<br>`sandloom/core/tests/test_subject_record.py::test_a_corrupt_record_fails_closed_and_issuance_repairs_it` |
| G-REVOKE-11 | On a revocation-store outage AUTH\_REVOCATION\_FAIL\_CLOSED decides (unset = closed in prod, open elsewhere): closed answers 503 on every authenticated route (the block cannot be confirmed, no copy answers); open accepts with a warning. | `sandloom/core/tests/test_auth_rotation_revocation.py::test_revocation_fail_closed_defaults_by_environment`<br>`sandloom/core/tests/test_revocation_freshness.py::test_redis_down_fail_closed`<br>`sandloom/core/tests/test_revocation_freshness.py::test_redis_down_fail_open_outside_prod`<br>`sandloom/core/tests/test_revocation_freshness.py::test_copy_expiring_during_block_read_fails_closed_on_outage` |
| G-REVOKE-12 | Revocation is strict on every authenticated route: each request reads the subject-state record live with one GET, so a revocation applies on the very next request in every process; several guards on one request share the one lookup. | `sandloom/core/tests/test_revocation_freshness.py::test_strict_default_sees_revocation_on_next_request`<br>`tests/integration/test_revocation_pipeline.py::test_c_in_flight_request_completes_and_the_next_one_is_rejected`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_revocation_lookup_runs_once_per_request`<br>`sandloom/core/tests/test_subject_record.py::test_the_hot_path_reads_one_key` |
| G-REVOKE-17 | before\_commit(callback) runs after the handler right before COMMIT and a raising check rolls the unit back; the commit-time re-check (subject\_commit\_check) runs before the COMMIT of every committing unit of the request, automatically on writes and writing GETs and under at\_commit() for explicit commit obligations (replacing revocation\_at\_commit()); it re-reads the caller's subject-state record live there, so a revocation or block written during the request answers 401 with nothing committed (also for named-database units). | `sandloom/core/tests/test_before_commit.py::test_a_failing_check_rolls_back_instead_of_committing`<br>`sandloom/core/tests/test_before_commit.py::test_subject_check_sees_a_block_written_during_the_request`<br>`sandloom_tenancy/tests/test_accept_authority_at_commit.py::test_block_while_accepting_rolls_the_acceptance_back`<br>`sandloom/core/tests/test_kernel_review_regressions.py::test_revocation_at_commit_covers_named_db_in_either_dependency_order`<br>`tests/integration/test_route_runtime.py::test_a_block_during_a_write_that_declares_nothing_refuses_and_rolls_back`<br>`tests/integration/test_route_runtime.py::test_a_get_that_writes_is_rechecked_and_a_reading_get_is_not`<br>`tests/integration/test_route_runtime.py::test_a_foreign_revocation_between_two_commits_refuses_the_second` |
| G-REVOKE-18 | While the commit-time re-check is armed, internal RPC sends described read methods unchanged and propagates remote commands (and methods of unknown kind) with the re-check in the MAC-covered context (rc: sub, iat\_us; the provider re-checks in its own unit before COMMIT) instead of refusing them, so no remote command commits unchecked; every HTTP request (incl. nested ASGI and internal RPC) starts a fresh authority context and restores the caller's on return. | `sandloom/core/tests/test_security_review_regressions.py::test_guarded_request_never_sends_remote_command`<br>`sandloom/core/tests/test_security_review_regressions.py::test_guarded_request_can_still_send_described_remote_read`<br>`sandloom/core/tests/test_security_review_regressions.py::test_rpc_http_boundary_isolates_and_restores_outer_commit_authority`<br>`sandloom/core/tests/test_security_review_regressions.py::test_new_http_request_does_not_inherit_outer_issue_floors` |
| G-REVOKE-19 | Revocation marks and blocks are read and written only through SECURITY\_CACHE\_INSTANCE and never move to IDEMPOTENCY\_CACHE\_INSTANCE. | `sandloom/core/tests/test_production_profile.py::test_security_state_uses_the_security_cache_instance`<br>`sandloom/core/tests/test_idempotency_cache_instance.py::test_revocation_marks_stay_in_the_security_cache` |
| G-REVOKE-21 | Every authenticated route reads the subject-state record sandloom:auth:subject:{id} live, with one GET memoised per request (no route-level freshness setting, no per-process copy); a revoked or blocked token answers 401 WWW-Authenticate: Bearer error="invalid\_token", a fail-closed outage 503. | `sandloom/core/tests/test_subject_record.py::test_the_hot_path_reads_one_key`<br>`sandloom/core/tests/test_revocation_freshness.py::test_strict_default_sees_revocation_on_next_request`<br>`sandloom/core/tests/test_revocation_freshness.py::test_revocation_in_one_process_invalidates_another`<br>`sandloom/core/tests/test_revocation_freshness.py::test_the_freshness_knobs_are_gone`<br>`tests/integration/test_platform_routes_strict.py::test_every_authenticated_platform_route_refuses_a_withdrawn_session`<br>`tests/perf_budgets/test_cache_round_trips.py::test_authenticated_get_budget` |
| G-REVOKE-22 | The commit-time re-check runs before the COMMIT of every committing unit of an armed request: automatically on user writes (a write or a queued effect commits), on a GET whose unit wrote (its on\_commit kept), and on every unit under at\_commit(); units that commit nothing skip it; a foreign revocation between two commits of one request refuses the second; operation-intent routes use release\_check. | `tests/integration/test_route_runtime.py::test_a_block_during_a_write_that_declares_nothing_refuses_and_rolls_back`<br>`tests/integration/test_route_runtime.py::test_a_get_that_writes_is_rechecked_and_a_reading_get_is_not`<br>`tests/integration/test_route_runtime.py::test_a_foreign_revocation_between_two_commits_refuses_the_second`<br>`sandloom/core/tests/test_kernel_review_regressions.py::test_revocation_at_commit_covers_named_db_in_either_dependency_order`<br>`sandloom/core/tests/test_kernel_review_regressions.py::test_nested_http_request_has_independent_commit_authority`<br>`sandloom/core/tests/test_kernel_review_regressions.py::test_reused_session_does_not_keep_an_old_requests_authority_check` |
| G-REVOKE-23 | Self-revocation is "check, then revoke": a unit revoking its own subject commits after the live check passed; a foreign revocation landing before the self-revoking call is still seen; a self-revocation inside a rolled-back savepoint contributes nothing; committed units carry the exemption to later units of the request, a remote command's included: the acknowledgement crosses out as a signed caller fact (usr.sr) and the provider's re-check skips the subject exactly as a local unit's does; over RPC the provider's self-revocations come back MAC-bound (sr); a replay of the self-revoking command (the same caller's retry, locally or the proxy's automatic retry) acknowledges it alike (kept with the command's dedupe record). | `tests/integration/test_route_runtime.py::test_a_route_revoking_its_own_subject_commits`<br>`tests/integration/test_route_runtime.py::test_a_foreign_revocation_before_the_self_revoking_call_is_still_seen`<br>`tests/integration/test_route_runtime.py::test_a_self_revocation_in_a_rolled_back_savepoint_contributes_nothing`<br>`sandloom/core/tests/test_security_review_regressions.py::test_rpc_http_boundary_isolates_and_restores_outer_commit_authority`<br>`tests/integration/test_durable_command_idempotency.py::test_an_acknowledged_self_revocation_crosses_rpc`<br>`tests/integration/test_durable_command_idempotency.py::test_a_durable_replay_keeps_the_acknowledged_self_revocation` |
| G-REVOKE-24 | A remote command under an armed re-check is propagated in the MAC-covered RPC context (rc: sub, iat\_us) and the provider re-checks before its own COMMIT (instead of the caller refusing to send it); described remote reads are sent unchanged. | `sandloom/core/tests/test_security_review_regressions.py::test_guarded_request_never_sends_remote_command`<br>`sandloom/core/tests/test_security_review_regressions.py::test_guarded_request_can_still_send_described_remote_read` |

### G-BLOCK

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-BLOCK-01 | The block is a value of the subject-state record (v1:blocked:, no TTL), read live on every authenticated request, never from a per-process copy, and rejects every token of the user, including ones minted after the block, with 401 'Account blocked' (WWW-Authenticate: Bearer error="invalid\_token", as G-REVOKE-21). | `sandloom/core/tests/test_revocation_freshness.py::test_block_applies_on_next_request_even_on_budget_routes`<br>`sandloom/core/tests/test_revocation_immediate_and_corrupt.py::test_any_block_value_blocks`<br>`sandloom_billing/tests/test_billing_route_classes.py::test_blocked_user_rejected_on_every_route_like_before`<br>`sandloom/core/tests/test_route_markers.py::test_differential_strict_budgeted_revoked_blocked`<br>`tests/integration/test_platform_routes_strict.py::test_every_authenticated_platform_route_refuses_a_withdrawn_session` |
| G-BLOCK-02 | block\_user must run in a unit of work with a reason, writes a required audit (auth.user.blocked) and writes the block value of the subject-state record LAST (after audit, immediate revocation and event); a cache outage raises BlockUnavailableError (503) and the audit rolls back. | `sandloom/core/tests/test_revocation_freshness.py::test_block_requires_uow_and_reason`<br>`sandloom/core/tests/test_revocation_freshness.py::test_block_mark_is_written_last`<br>`sandloom/core/tests/test_revocation_freshness.py::test_block_write_outage_is_503_and_rolls_back_audit` |
| G-BLOCK-03 | unblock\_user (audited auth.user.unblocked) replaces the block value of the subject-state record with after: by compare-and-set only after its unit of work commits (on\_commit); a rolled-back or failed unblock leaves the user blocked. | `sandloom/core/tests/test_revocation_freshness.py::test_unblock_lifts_the_mark_only_after_commit`<br>`sandloom_identity/tests/api/test_user_safety.py::test_block_status_unblock_round_trip`<br>`sandloom/core/tests/test_subject_record.py::test_a_rolled_back_unblock_keeps_the_block` |
| G-BLOCK-04 | Every bundled token issuer (password login, password+2FA login, SSO and SSO second-factor completion, refresh exchange) calls ensure\_user\_not\_blocked live before any side effect (no token, no refresh rotation, SSO state not consumed), answering 401 'Account blocked'; a cache outage follows AUTH\_REVOCATION\_FAIL\_CLOSED. | `sandloom_identity/tests/api/test_emergency_block.py::test_blocked_user_cannot_login_or_refresh`<br>`sandloom_identity/tests/api/test_emergency_block.py::test_blocked_user_cannot_complete_sso_or_sso_2fa`<br>`sandloom_identity/tests/api/test_emergency_block.py::test_blocked_user_cannot_login_with_password_2fa`<br>`sandloom_identity/tests/api/test_emergency_block.py::test_issuer_block_check_on_cache_outage`<br>**gap (M)**: No generic check forces a custom (app-defined) issuer to call ensure\_user\_not\_blocked; the rule is documentation-only for third-party issuers. |
| G-BLOCK-05 | On commit block\_user ends the user's sessions: all access tokens issued so far are revoked and core.auth.user\_blocked.v1 is published, whose identity consumer revokes every refresh token, so lifting the block requires a fresh login. | `sandloom_identity/tests/api/test_emergency_block.py::test_block_ends_sessions_on_commit` |
| G-BLOCK-06 | One subject-state record: v1:blocked: (no TTL) outranks every v1:after:, so a revocation never unblocks; the Redis writer is one Lua script, the portable writer a compare-and-set loop that refuses to run on a store without CAS. | `sandloom/core/tests/test_subject_record.py::test_value_domain`<br>`sandloom/core/tests/test_subject_record.py::test_portable_order`<br>`sandloom/core/tests/test_subject_record.py::test_portable_writer_refuses_without_cas`<br>`sandloom/core/tests/test_subject_record.py::test_a_revocation_concurrent_with_a_block_never_unblocks`<br>`sandloom/core/tests/test_subject_record.py::test_redis_subject_state_order_and_ttl`<br>`sandloom/core/tests/test_subject_record.py::test_redis_concurrent_block_and_revocations_end_blocked` |
| G-BLOCK-07 | Unblock replaces blocked with after: by compare-and-set after commit and never deletes the record: tokens issued before the block stay revoked, and a rolled-back unblock keeps the block. | `sandloom/core/tests/test_subject_record.py::test_unblock_never_resurrects_pre_block_tokens`<br>`sandloom/core/tests/test_subject_record.py::test_a_rolled_back_unblock_keeps_the_block`<br>`sandloom/core/tests/test_subject_record.py::test_redis_unblock_swaps_and_never_deletes` |

### G-OPCLASS

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-OPCLASS-03 | The platform route policy is pinned: each module's routes equal its committed routes.snapshot.json (format 3: the full resolved policy per route and method) and the kernel-mounted routes carry the kernel's policies, so adding, removing or changing the resolved policy of a route fails CI; billing's routes are exactly the approved set, declare no freshness budget and are always strict. | `tests/architecture/test_operation_classes.py::test_module_routes_match_their_snapshots`<br>`tests/architecture/test_operation_classes.py::test_kernel_routes_carry_the_kernel_classes`<br>`tests/architecture/test_operation_classes.py::test_a_stale_snapshot_names_the_route_and_the_fix`<br>`sandloom_billing/tests/test_billing_route_classes.py::test_every_billing_route_is_classified`<br>`sandloom_billing/tests/test_billing_route_classes.py::test_read_routes_are_strict_today_and_declare_no_budget`<br>`sandloom_billing/tests/test_billing_route_classes.py::test_financial_routes_are_always_strict` |
| G-OPCLASS-04 | create\_app prunes the route-policy markers (public, no\_idempotency, replay\_safe, sensitive, operation\_intent, writes, allow\_multi\_db\_write) from route.dependant (never executed per request) while keeping the declaration readable; responses, OpenAPI and session behaviour are unchanged, and a marker bound to a parameter of another dependency is never pruned. | `sandloom/core/tests/test_route_markers.py::test_create_app_prunes_every_marker`<br>`sandloom/core/tests/test_route_markers.py::test_pruned_markers_are_no_longer_called`<br>`sandloom/core/tests/test_route_markers.py::test_openapi_is_identical`<br>`sandloom/core/tests/test_route_markers.py::test_marker_bound_to_a_parameter_is_kept` |
| G-OPCLASS-05 | Identity admin-gated RBAC / user-admin mutations re-check the actor inside the request unit (admin\_in\_transaction: RBAC advisory lock + actor admin assignment and active user row FOR SHARE): a token still claiming admin after revocation gets 403, a concurrent demotion serialises, and two admins demoting each other do not deadlock. | `sandloom_identity/tests/api/test_admin_in_transaction.py::test_admin_claim_without_the_role_cannot_administer`<br>`sandloom_identity/tests/api/test_admin_in_transaction.py::test_demotion_committing_mid_request_refuses_the_admin_action`<br>`sandloom_identity/tests/api/test_admin_in_transaction.py::test_admin_action_in_flight_serialises_the_demotion`<br>`sandloom_identity/tests/api/test_admin_in_transaction.py::test_two_admins_demoting_each_other_do_not_deadlock` |
| G-OPCLASS-06 | Tenancy admin-gated writes authorizing on the acting member (role change, removal, org update, invitation create/revoke) lock the actor row FOR SHARE and the target FOR UPDATE in the write's unit, in user\_id order: a demotion/removal committed first is seen (403/404), an action committed first serialises the demotion, no deadlock. | `sandloom_tenancy/tests/test_actor_row_lock.py::test_demoted_actor_cannot_remove_a_member`<br>`sandloom_tenancy/tests/test_actor_row_lock.py::test_demoted_actor_cannot_invite`<br>`sandloom_tenancy/tests/test_actor_row_lock.py::test_action_committed_first_serialises_the_demotion`<br>`sandloom_tenancy/tests/test_actor_row_lock.py::test_two_admins_acting_on_each_other_do_not_deadlock` |
| G-OPCLASS-07 | Platform owner console routes: owner directory / billing / kernel per-module reads are platform-admin gated (platform admin only, tenant membership grants nothing, no secrets leaked); outbox requeue / streams replay are platform-admin gated writes (fail-closed Idempotency-Key, required audit, before-commit revocation check); credit adjustment is a money write with ledger key, balance effect and audit in one unit. | `sandloom_billing/tests/test_platform_admin.py::test_all_operator_reads_deny_tenant_accounts`<br>`sandloom/core/tests/test_admin_operations.py::test_views_are_operator_only_and_leak_no_secrets`<br>`sandloom/core/tests/test_admin_operations.py::test_outbox_requeue_is_audited_atomically_and_idempotent`<br>`sandloom_billing/tests/test_platform_admin.py::test_committed_adjustment_is_concurrent_safe_and_audit_failure_rolls_back` |

### G-ROUTE

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-ROUTE-01 | create\_app refuses (RoutePolicyError, SANDLOOM-B008) an API route that neither resolves an authenticator (@authenticates, user or kind="service") nor declares public(reason=...), one that does both, an empty reason, or two different public reasons; the error names the fix. Apps assert the same deny-by-default with sandloom.core.testing.route\_policy(route). | `sandloom/core/tests/test_route_policy_auth.py::test_an_unauthenticated_untagged_route_is_refused_with_the_fix`<br>`sandloom/core/tests/test_route_policy_auth.py::test_public_and_an_authenticator_together_are_refused`<br>`sandloom/core/tests/test_route_policy_auth.py::test_an_empty_reason_is_refused_at_declaration`<br>`sandloom/core/tests/test_route_policy_auth.py::test_create_app_refuses_an_unauthenticated_route`<br>`sandloom/core/tests/test_route_policy_auth.py::test_every_platform_route_is_authenticated_or_public`<br>`tests/architecture/test_operation_classes.py::test_every_platform_route_has_one_consistent_class`<br>`tests/architecture/test_operation_classes.py::test_unclassified_route_fails_the_check`<br>`tests/architecture/test_operation_classes.py::test_two_different_classes_fail`<br>`examples/projecthub/tests/test_smoke.py::test_every_route_declares_its_operation_class` |
| G-ROUTE-02 | public(reason=) is recorded with its reason; token issuers (public(issues\_tokens=True, reason=...)) keep the live issuance check; a router-level public() covers its routes; the bundled public routes are exactly the reviewed set. | `sandloom/core/tests/test_route_policy_auth.py::test_public_with_a_reason_passes_and_is_recorded`<br>`sandloom/core/tests/test_route_policy_auth.py::test_router_level_public_covers_its_routes`<br>`sandloom/core/tests/test_route_policy_auth.py::test_bundled_public_routes_are_exactly_the_reviewed_set`<br>`tests/architecture/test_operation_classes.py::test_router_level_class_covers_its_routes` |
| G-ROUTE-03 | PSP webhooks are public(reason=...) routes, not service principals, and verify their signature inside the handler (verification unchanged: an invalid or missing signature is rejected). | `sandloom/core/tests/test_route_policy_auth.py::test_bundled_public_routes_are_exactly_the_reviewed_set`<br>`sandloom_billing/tests/test_webhooks.py::TestWebhookSecurity::test_invalid_signature_rejected`<br>`sandloom_billing/tests/test_webhooks.py::TestWebhookSecurity::test_missing_signature_rejected` |
| G-ROUTE-04 | Each module's routes.snapshot.json (format 3) records the static resolved policy per route and method: auth, effect, session, risk with its sources, idempotency (mode), audit, step\_up\_max\_age\_s, public\_reason, issues\_tokens, operation\_intent, write\_store, multi\_db\_write, and authz (the custom authorization dependencies marked with sandloom.sdk.web.authorizes, in resolution order); drift, including a weakening such as a dropped risk, fails the module's tests with the fix (SANDLOOM-T001). sandloom generate never writes a weaker policy over the committed snapshot, and sandloom test never writes any generated file (authentication removed or the route made public, a risk removed, idempotency removed / no longer fail-closed / no longer durable, the session check weakened, the automatic audit dropped, step-up removed or relaxed, an authorization dependency removed): sandloom generate fails with SANDLOOM-T001 before anything is written, names sandloom routes snapshot --write, and sandloom test fails, until sandloom routes snapshot --write approves it explicitly, naming each approved weakening (the snapshot diff records it). Idempotency is compared by its properties (present, fail-closed, durable), derived from every mode the snapshot writes (none / none (reason), automatic S and automatic B (reason): a durable fail-closed record; explicit durable / cache / in-flight with or without fail-closed: only the durable kind is durable); losing any property is a weakening whatever the spelling (automatic S to explicit in-flight, fail-closed loses durability), and an unknown mode is never accepted silently. | `tests/architecture/test_operation_classes.py::test_module_routes_match_their_snapshots`<br>`tests/architecture/test_operation_classes.py::test_a_stale_snapshot_names_the_route_and_the_fix`<br>`tests/sdk/test_sdk_generated.py::test_every_weakened_route_policy_is_incompatible`<br>`tests/sdk/test_sdk_generated.py::test_a_route_made_public_is_incompatible`<br>`tests/sdk/test_sdk_generated.py::test_a_stronger_route_policy_is_compatible`<br>`tests/sdk/test_sdk_generated.py::test_sandloom_test_refuses_a_weakened_route_snapshot_and_the_write_approves_it`<br>`tests/sdk/test_sdk_generated.py::test_generate_refuses_a_weakened_route_and_names_the_approval`<br>`tests/sdk/test_sdk_generated.py::test_every_idempotency_mode_transition_that_loses_a_property_is_flagged`<br>`tests/sdk/test_sdk_generated.py::test_an_idempotency_mode_that_keeps_every_property_is_compatible`<br>`tests/sdk/test_sdk_generated.py::test_every_snapshot_idempotency_spelling_has_properties`<br>`tests/architecture/test_operation_classes.py::test_kernel_routes_carry_the_kernel_classes`<br>`tests/integration/test_automatic_idempotency.py::test_the_snapshot_records_the_mode`<br>`sandloom/core/tests/test_route_authorizes.py::test_the_snapshot_records_custom_authorization_dependencies` |
| G-ROUTE-05 | The money / security effects a module's own tests see on a route are recorded as INFORMATIONAL observed\_effects in its routes.snapshot.json: the route runtime hands each request's risk effects to the test harness's observer (none in production; a failing observer never fails a request), the sandloom.sdk.testing plugin records them per module when SANDLOOM\_TEST\_OBSERVE\_EFFECTS is set (sandloom test outside --ci), sandloom routes snapshot --write attaches them, and the snapshot check ignores them in either direction while a real policy change still fails. | `sandloom/core/tests/test_observed_effects.py::test_the_runtime_reports_a_requests_effects_to_the_observer`<br>`sandloom/core/tests/test_observed_effects.py::test_observed_effects_are_written_but_never_checked`<br>`sandloom/core/tests/test_observed_effects.py::test_the_harness_records_per_module_and_sandloom_test_opts_in` |

### G-SENS

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-SENS-01 | The framework's admin gates carry the admin risk statically: require\_platform\_admin, require(Role("admin")), require(OrgRole("admin"\|"owner")) (and an Or of them), tenancy's require\_org\_role("admin"\|"owner") and the platform console's operator gate; member gates and Not do not. | `tests/integration/test_sensitive_risk.py::test_admin_gates_carry_the_admin_risk_and_member_gates_do_not`<br>`tests/integration/test_sensitive_risk.py::test_every_platform_route_behind_an_admin_gate_resolves_the_admin_risk`<br>`tests/architecture/test_operation_classes.py::test_kernel_routes_carry_the_kernel_classes` |
| G-SENS-02 | Money risk is declared at its source: @command(..., risk="money") (the L2 contract\_method(command=True, risk="money")) (recorded in the describe document and the contract snapshot; billing credits.spend), the MoneyLedger mixin (a validated NOT NULL + UNIQUE operation key; billing's credit ledger; an INSERT in a request is a money effect) and @sensitive\_effect("money") (billing intents). | `tests/integration/test_sensitive_risk.py::test_a_money_command_records_its_risk_in_the_contract_schema`<br>`tests/integration/test_sensitive_risk.py::test_a_money_ledger_must_name_a_not_null_unique_operation_key`<br>`tests/integration/test_sensitive_risk.py::test_a_money_route_commits_only_with_a_deduped_money_effect_or_a_key`<br>`sandloom/core/tests/test_command_risk_parity.py::test_a_money_command_marks_the_callers_request_locally_and_remotely` |
| G-SENS-03 | Security risk: identity's credential services (password change, 2FA init / enable / disable / backup-code regeneration, SSO link / unlink) are @sensitive\_effect("security", step\_up=300); mark\_reauthenticated() after the current-password check (or a recent auth\_time) satisfies it, so identity's behaviour is unchanged. The step-up is enforced identically when the effect runs in a contract command served over RPC: the signed caller facts carry the end user's auth\_time and the request's re-authentication to the provider, whose call runs in a request scope built from them (with or without a commit-time re-check context, on an operation-intent route too), so stale auth is 401 and fresh auth or a re-authentication passes, local and remote alike; a re-authentication or a pending step-up the remote execution leaves comes back with its response. | `tests/integration/test_sensitive_risk.py::test_a_security_effect_needs_a_recent_auth_time_or_a_reauthentication`<br>`tests/integration/test_rpc_caller_parity.py::test_step_up_is_enforced_alike_local_and_split`<br>`tests/integration/test_rpc_caller_parity.py::test_a_reauthentication_in_a_remote_command_reaches_the_request`<br>`tests/integration/test_rpc_caller_parity.py::test_a_split_call_matches_the_local_one` |
| G-SENS-04 | Every sensitive write gets exactly one automatic, mandatory audit row (sandloom.sensitive\_write) in the request's writing unit: actor, route template, method, path parameters, risks with their sources and the route's status; never the body; no row for a read or a refused request; a money command marks the caller's request alike locally and remotely (from the describe document before the call, and from the MAC-bound risks of the response: every risk the remote execution observed, its nested calls' included, so an untagged command calling a money command marks the request as it would locally; the risks travel on error responses too, merged before the caller raises, so a wrapper that raises after its nested money command committed still marks the request; a command REPLAY, local or remote (the proxy's automatic retry after a lost answer included), marks the caller exactly like its first execution: the durable command record (and the cache fallback) keeps the risks the execution observed and the replay re-applies them, its on\_commit callbacks' included (round 5: the cache record is written after the callbacks ran; the durable record commits with the body's effects plus what each queued callback declares - a @sensitive\_effect callback's risk and step-up, fail closed - and is corrected with what the callbacks actually left once they ran, a closure's included; a process death between the commit and that correction replays the declared callbacks' risks and nothing of a closure; round 6: the correction - and the cache record - is written on EVERY exit after the commit, a cancelled callback (a request timeout) included, with what the callbacks left until then plus what the queued ones declare when they did not all finish (fail closed), in a task shielded from the caller's cancellation (anyio cancels again at every await), bounded (5 s) and never raising into the caller: a closure that completed a money effect before its request was cancelled is on the record, so a later replay owes the audit too)), so the audit does not change after a split or a retry; such a command commits in its own unit, as does a risky effect run inside any contract unit (locally too), so the request writes its row even when its own units wrote nothing (the route runtime opens the request unit when the handler opened none). | `tests/integration/test_sensitive_risk.py::test_a_sensitive_write_gets_one_audit_row_and_a_read_or_failure_none`<br>`tests/integration/test_sensitive_risk.py::test_injected_session_keeps_risk_bound_through_dependency_commit`<br>`tests/integration/test_bundled_sensitive_operations.py::test_operator_revocation_reports_admin_risk_on_execution_and_replay`<br>`sandloom/core/tests/test_command_risk_parity.py::test_a_money_command_marks_the_callers_request_locally_and_remotely`<br>`tests/integration/test_sensitive_risk.py::test_a_command_only_money_route_gets_its_audit_row`<br>`sandloom/core/tests/test_command_risk_parity.py::test_a_nested_money_command_marks_the_callers_request_locally_and_remotely`<br>`tests/integration/test_rpc_caller_parity.py::test_risks_of_a_failing_command_mark_the_request_local_and_split`<br>`tests/integration/test_rpc_caller_parity.py::test_a_split_call_matches_the_local_one`<br>`tests/integration/test_rpc_caller_parity.py::test_a_replayed_command_marks_the_request_like_its_first_execution`<br>`tests/integration/test_durable_command_idempotency.py::test_a_durable_replay_marks_the_request_like_its_first_execution`<br>`tests/integration/test_durable_command_idempotency.py::test_a_durable_replay_marks_the_request_with_its_on_commit_callbacks`<br>`tests/integration/test_durable_command_idempotency.py::test_a_crash_before_the_callbacks_leaves_a_fail_closed_record`<br>`tests/integration/test_rpc_caller_parity.py::test_a_replay_marks_the_request_with_its_on_commit_callbacks`<br>`tests/integration/test_durable_command_idempotency.py::test_a_cancelled_on_commit_callback_keeps_its_effects_on_the_record`<br>`tests/integration/test_rpc_caller_parity.py::test_a_cancelled_on_commit_callback_keeps_its_effects_on_the_cache_record`<br>`sandloom/core/tests/test_contract_finish_record.py` |
| G-SENS-05 | Money dedupe is mandatory: a money effect needs a dedupe record (its own operation\_key, the request's request\_operation\_key(), a command key or a ledger key); a client without Idempotency-Key gets 428 (SANDLOOM-R010); a route that cannot dedupe the key it got is SANDLOOM-R009 (a route whose only money effect is a command is held to the same rules: the command key is its dedupe record); a sensitive("money") write committing without either is refused before COMMIT; at startup a money route whose idempotency guard is the response-storing cache form or fail-open is refused (SANDLOOM-B019), while durable=True or store\_response=False + fail\_closed=True guards are accepted. | `tests/integration/test_sensitive_risk.py::test_injected_session_keeps_risk_bound_through_dependency_commit`<br>`tests/integration/test_sensitive_risk.py::test_a_money_effect_needs_a_dedupe_record_or_answers_428`<br>`tests/integration/test_sensitive_risk.py::test_a_key_the_route_cannot_dedupe_is_a_developer_error`<br>`tests/integration/test_sensitive_risk.py::test_a_money_route_commits_only_with_a_deduped_money_effect_or_a_key`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_f_route_refuses_a_replay_only_guard`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_f_route_accepts_durable_and_endpoint_owned_guards`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_bundled_f_routes_pass_and_the_platform_wraps_no_route`<br>`tests/integration/test_sensitive_risk.py::test_a_command_only_money_route_gets_its_audit_row` |
| G-SENS-06 | Step-up (RFC 9470): an effect declaring step\_up=N (security: 300 s) needs auth\_time (never iat) at most N s old or mark\_reauthenticated() in the request, else 401 WWW-Authenticate: Bearer error="insufficient\_user\_authentication", max\_age=N (SANDLOOM-R011); checked at entry, at the effect's return and before the writing unit's COMMIT; a sensitive("security") route refuses before the response, reads included. | `tests/integration/test_sensitive_risk.py::test_a_security_effect_needs_a_recent_auth_time_or_a_reauthentication`<br>`tests/integration/test_sensitive_risk.py::test_a_security_route_refuses_before_the_response_even_on_a_read` |
| G-SENS-07 | Risk is a join: Requirement.risks() is the union over And / Or and empty under Not; a route's resolved risk is the union of its sources (admin gates, @sensitive\_effect dependencies, sensitive() markers) whatever their order or repetition, each naming its sources; the derived step-up is the strictest of its contributors' (obligations are a homomorphism). | `tests/laws/test_risk_join_laws.py::test_requirement_risks_are_a_union`<br>`tests/laws/test_risk_join_laws.py::test_route_risk_is_the_join_of_its_sources`<br>`tests/laws/test_risk_join_laws.py::test_obligations_are_a_homomorphism` |

### G-AUTHZ

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-AUTHZ-01 | Authorization decisions are three-valued (allow / deny / indeterminate) and keep their status: a definite Subscribed/Credits denial (of= an explicit Account) is 402, an unregistered or unavailable fact or a missing contract is 501, an unreachable owner is 503; an indeterminate branch never yields allow through Or/Not. | `tests/integration/test_shared_authz_review.py::test_http_subscribed_denial_is_402`<br>`tests/integration/test_shared_authz_review.py::test_http_unregistered_fact_is_501_not_403`<br>`tests/integration/test_shared_authz_review.py::test_or_with_erroring_branch_does_not_allow`<br>`tests/integration/test_shared_authz_review.py::test_double_negation_of_error_still_denies`<br>`tests/integration/test_access_facts_split.py::test_unreachable_owner_is_503` |
| G-AUTHZ-02 | require(...) evaluates the cheapest tier first (claims -\> resource -\> live) and short-circuits; a claims-only policy such as require(Subscribed(of=Account.user())) opens no DB session and does no I/O. | `tests/integration/test_shared_authz_review.py::test_http_claims_only_policy_opens_no_db_session`<br>`tests/integration/test_authz_algebra.py::test_cheap_claim_short_circuits_before_resource_load`<br>`tests/integration/test_access_subsystem.py::test_and_returns_first_failing_cheapest_first` |
| G-AUTHZ-03 | SubscriptionActive(of=...) and Credits(n, of=...) (of= required) are live and authoritative, resolved through the owner's contract (local, or the SERVICE\_BINDINGS RPC proxy when remote). | `tests/integration/test_shared_authz_review.py::test_credits_uses_remote_binding_proxy`<br>`tests/integration/test_shared_authz_review.py::test_subscription_active_uses_remote_binding_proxy`<br>`tests/integration/test_shared_authz_review.py::test_http_credits_denial_is_402_with_reason_detail` |
| G-AUTHZ-04 | Registering an access fact auto-exposes .facts.v1; a process without the owner that binds it in SERVICE\_BINDINGS gets the same decision as the monolith, with the declarative ClaimRule travelling over RPC (describe, cached). | `tests/integration/test_access_facts_split.py::test_entitled_subscription_same_decision_in_split`<br>`tests/integration/test_access_facts_split.py::test_owner_settings_drive_the_remote_claim_rule`<br>`tests/integration/test_access_facts_split.py::test_describe_is_declarative_and_cached`<br>`tests/integration/test_access_facts_split.py::test_unbound_fact_is_501_not_500` |
| G-AUTHZ-05 | Live adapters are consumer-side configuration: a registered adapter overrides the owner's contract, and registering one on the provider side warns. | `tests/integration/test_shared_authz_review.py::test_registered_adapter_overrides_contract`<br>`tests/integration/test_shared_authz_review.py::test_provider_side_live_adapter_registration_warns` |
| G-AUTHZ-06 | require(...) as a route dependency rejects resource-loading policies (only claim and live policies are allowed there). | `tests/integration/test_authz_algebra.py::test_require_rejects_resource_policy`<br>`tests/integration/test_authz_algebra.py::test_require_allows_claim_and_live_policies` |
| G-AUTHZ-07 | Org-scoped billing facts take the organization from the path or the bound tenant, never from the user id; without an org context the decision is indeterminate and a missing org can never be negated into allow. | `tests/integration/test_shared_authz_review.py::test_org_credits_use_org_from_path_never_user_id`<br>`tests/integration/test_shared_authz_review.py::test_org_credits_use_bound_tenant`<br>`tests/integration/test_shared_authz_review.py::test_org_credits_without_org_context_is_indeterminate`<br>`tests/integration/test_shared_authz_review.py::test_org_role_missing_org_cannot_be_negated_into_allow` |
| G-AUTHZ-08 | Authorization denials are recorded through the structured deny log and access\_denied\_\* metrics (a denied request rolls back, so it is not audited). | `tests/integration/test_access_subsystem.py::test_enforce_denies_and_records_metric` |
| G-AUTHZ-09 | Subscribed, SubscriptionActive, Credits, Feature and WithinLimit require of=; a missing or non-Account value raises AuthzSubjectMissingError (a TypeError, SANDLOOM-B017) naming the three choices (Account.user(), Account.org(param=), Account.bound\_tenant()). | `tests/integration/test_authz_subjects.py::test_every_billing_atom_requires_of` |
| G-AUTHZ-10 | Account.org(param=) resolves the path, then the query parameter, else X-Tenant-Id, else the bound tenant, validated as before; no organization, or the caller's own personal tenant, is indeterminate and never the caller's user id; Account.user() is the caller's own account on any route. Pinned by a property over random route shapes and bindings. | `tests/integration/test_authz_subjects.py::test_route_shapes_resolve_the_stated_account_only`<br>`tests/integration/test_authz_subjects.py::test_no_resolution_returns_a_foreign_account`<br>`examples/helpdesk/helpdesk/modules/tickets/tests/test_tickets_api.py::TestTickets::test_export_requires_subscription` |
| G-AUTHZ-11 | Account.bound\_tenant() resolves by tenant kind: personal is the user, an organization confirmed by the membership contract is the org, canonical / unknown / unbound is indeterminate; tenant\_kind() is authoritative only and memoised per request. | `tests/integration/test_authz_subjects.py::test_bound_tenant_resolves_by_tenant_kind`<br>`tests/integration/test_authz_subjects.py::test_tenant_kind_is_authoritative_and_memoised` |
| G-AUTHZ-12 | Feature / WithinLimit with an organization account read the owner's organization.features / organization.limits facts live (501 without a registered fact); with Account.user() they read the claims as before. | `tests/integration/test_authz_subjects.py::test_org_feature_and_limit_need_the_owners_fact` |
| G-AUTHZ-13 | The authorization algebra is unchanged (spec §8.3), pinned by properties over random And / Or / Not trees of fixed-outcome, evaluation-counting atoms against an independent model of today's rule: the value, the atoms evaluated and their order (cheapest tier first, short-circuit, no extra evaluation), fail-closed soundness (an allow holds under every two-valued completion of the indeterminate atoms), Not(U) = U, Or allowing on a genuine allow beside an indeterminate, evaluate agreeing with decide; the known order dependence (\~(F &amp; U) allows, \~(U &amp; F) does not) is kept. | `tests/laws/test_authz_algebra_laws.py::test_value_and_evaluations_equal_the_documented_rule`<br>`tests/laws/test_authz_algebra_laws.py::test_an_allow_holds_under_every_completion_of_the_indeterminate`<br>`tests/laws/test_authz_algebra_laws.py::test_not_keeps_an_indeterminate`<br>`tests/laws/test_authz_algebra_laws.py::test_or_allows_on_a_genuine_allow_beside_an_indeterminate`<br>`tests/laws/test_authz_algebra_laws.py::test_evaluate_agrees_with_decide`<br>`tests/laws/test_authz_algebra_laws.py::test_the_known_order_dependence_is_kept` |

### G-GUARD

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-GUARD-01 | create\_app refuses (GuardOrderError) a route where FastAPI resolves an idempotency\_guard before its authenticating dependency; router-level auth counts as preceding, and a custom authenticator counts once marked @authenticates. | `sandloom/core/tests/test_guard_order.py::test_guard_in_route_dependencies_before_param_auth_is_refused`<br>`sandloom/core/tests/test_guard_order.py::test_guard_param_before_auth_param_is_refused`<br>`sandloom/core/tests/test_guard_order.py::test_custom_authenticator_counts_once_marked`<br>`sandloom/core/tests/test_guard_order.py::test_bundled_app_routes_pass`<br>**gap (S)**: Tests call the order checker directly; no test boots create\_app with a misordered route to prove the startup wiring. |
| G-GUARD-02 | A valid access token's revocation and block state is checked before an idempotency\_guard reserves a key or replays a stored response, even when the guard is listed before the endpoint's auth dependency. | `sandloom/core/tests/test_security_review_regressions.py::test_replay_checks_live_auth_before_cache_lookup` |

### G-HTTP

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-HTTP-01 | The sliding-window rate limiter is always enforced in prod (RATE\_LIMIT\_ENABLED only forces it elsewhere), blocks boundary bursts, and a 429 carries Retry-After; on a cache outage it fails open by default or answers 503 when configured fail-closed. | `sandloom/core/tests/test_http_rate_limit.py::test_rate_limiter_exceeds_limit`<br>`sandloom/core/tests/test_http_rate_limit.py::test_rate_limiter_bypassed_outside_prod`<br>`sandloom/core/tests/test_client_ip.py::test_sliding_window_blocks_boundary_burst`<br>`sandloom/core/tests/test_http_rate_limit.py::test_rate_limiter_fail_closed_returns_503_on_cache_outage` |
| G-HTTP-02 | The global per-IP floor (RATE\_LIMIT\_GLOBAL\_REQUESTS) is off at 0 by default, when set limits every route including unknown routes / 404s per IP, and fails open on a cache outage. | `sandloom/core/tests/test_http_rate_limit.py::test_global_floor_is_off_by_default`<br>`sandloom/core/tests/test_http_rate_limit.py::test_global_floor_limits_unknown_routes_per_ip`<br>`sandloom/core/tests/test_http_rate_limit.py::test_global_floor_fails_open_on_cache_outage` |
| G-HTTP-03 | X-Forwarded-For is honoured only with TRUST\_PROXY\_HEADERS=true and an immediate peer inside TRUSTED\_PROXY\_IPS, walked right-to-left skipping trusted hops (duplicate XFF lines combined in wire order); with trust on but no CIDRs prod uses the peer and dev the rightmost hop; invalid CIDRs fail settings. | `sandloom/core/tests/test_client_ip.py::test_spoofed_leftmost_xff_is_ignored`<br>`sandloom/core/tests/test_client_ip.py::test_walks_right_to_left_skipping_trusted_hops`<br>`sandloom/core/tests/test_client_ip.py::test_no_allowlist_prod_uses_peer_dev_uses_rightmost`<br>`sandloom/core/tests/test_security_review_regressions.py::test_duplicate_xff_lines_preserve_proxy_appended_client` |
| G-HTTP-04 | /metrics, /diagnostics/*, /contracts/* and /health details are closed (404) without a valid INTERNAL\_API\_TOKEN in every environment, opened only by INTERNAL\_ENDPOINTS\_OPEN\_IN\_DEV outside prod; METRICS\_TOKEN unlocks /metrics only. | `sandloom/core/tests/test_internal_auth.py::test_closed_in_dev_without_token_by_default`<br>`sandloom/core/tests/test_internal_auth.py::test_dev_opt_in_ignored_in_prod`<br>`sandloom/core/tests/test_internal_auth.py::test_metrics_token_unlocks_only_metrics`<br>`sandloom/core/tests/test_internal_auth.py::test_token_required_when_configured` |
| G-HTTP-05 | In prod INTERNAL\_API\_TOKEN and METRICS\_TOKEN must be long and random, and METRICS\_TOKEN must differ from INTERNAL\_API\_TOKEN (settings validation). | `sandloom/core/tests/test_ops_security_hardening.py::test_prod_validates_internal_and_metrics_tokens` |
| G-HTTP-06 | MAX\_REQUEST\_BODY\_BYTES caps every request body globally with 413, chunked bodies and every Content-Length header included. | `sandloom/core/tests/test_ops_security_hardening.py::test_chunked_oversize_json_body_is_413`<br>`sandloom/core/tests/test_api_robustness.py::test_body_cap_fast_path_considers_every_content_length` |
| G-HTTP-07 | Interactive docs (/docs etc.) are off in prod unless DOCS\_ENABLED is set. | `sandloom/core/tests/test_api_robustness.py::test_docs_exposed_property` |
| G-HTTP-08 | CORS follows CORS\_ORIGINS; a wildcard origin disables credentials, and masked 500 responses still carry the CORS headers. | `sandloom/core/tests/test_review_fixes.py::test_cors_wildcard_disables_credentials`<br>`sandloom/core/tests/test_review_fixes.py::test_cors_explicit_origin_keeps_credentials`<br>`sandloom/core/tests/test_ops_security_hardening.py::test_masked_500_carries_cors_headers` |
| G-HTTP-09 | JSON API responses carry CSP default-src 'none'; frame-ancestors 'none', restricted permissions and COOP same-origin without breaking HTML/Swagger asset policy. | `sandloom/core/tests/test_security_review_regressions.py::test_json_security_headers_do_not_break_html_assets` |
| G-HTTP-10 | Public /health and /ready return check statuses only (module/consumer topology hidden without X-Internal-Token) and are cached for HEALTH\_CACHE\_TTL\_SECONDS so anonymous traffic cannot fan out probes. | `sandloom/core/tests/test_ops_security_hardening.py::test_public_health_probes_are_cached_and_topology_hidden` |
| G-HTTP-11 | Logs redact sensitive extra keys by substring and scrub credential-shaped values from messages; Sentry events pass the same redaction. | `sandloom/core/tests/test_ops_security_hardening.py::test_log_extras_redacted_by_substring`<br>`sandloom/core/tests/test_ops_security_hardening.py::test_free_text_messages_are_scrubbed`<br>`sandloom/core/tests/test_ops_security_hardening.py::test_sentry_before_send_scrubs_event` |
| G-HTTP-12 | User (authenticated) routes answer Cache-Control: no-store unless the route sets its own Cache-Control. | `tests/integration/test_route_runtime.py::test_user_routes_are_no_store_unless_they_set_cache_control` |

### G-BILLING

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-BILLING-01 | Checkout and customer-portal run as operation\_intent routes, authorize on get\_member\_state (role + membership version) and commit a pending operation intent BEFORE any PSP call, keying the PSP call with the intent id (sandloom:intent:): an owner outage at authorize creates nothing (503), a PSP timeout leaves the intent pending (503) and a retry reuses the same PSP session, a definitive PSP 4xx fails the intent (502). | `sandloom_billing/tests/test_operation_intents.py::test_checkout_persists_an_issued_intent_keyed_to_the_psp_session`<br>`sandloom_billing/tests/test_operation_intents.py::test_org_checkout_records_the_authorized_membership_version`<br>`sandloom_billing/tests/test_operation_intents.py::test_psp_timeout_then_retry_reuses_the_intent_and_the_psp_session`<br>`sandloom_billing/tests/test_operation_intents.py::test_a_definitive_psp_rejection_fails_the_intent`<br>`sandloom_billing/tests/test_operation_intents.py::test_owner_unreachable_at_authorize_creates_nothing` |
| G-BILLING-02 | Right before the issue commit the final re-read, wrapped in release\_check, checks the membership state (role lost 403, other change 409, owner unreachable fail closed) and the caller's subject-state record (401); any refusal revokes the pending intent (audited), expires the checkout session and never returns the URL (a portal URL is never handed out). | `sandloom_billing/tests/test_operation_intents.py::test_role_loss_during_checkout_revokes_the_intent_and_expires_the_session`<br>`sandloom_billing/tests/test_operation_intents.py::test_role_loss_during_portal_revokes_the_intent_and_withholds_the_url`<br>`sandloom_billing/tests/test_operation_intents.py::test_block_during_personal_checkout_revokes_and_expires`<br>`sandloom_billing/tests/test_f_class_authority.py::test_other_membership_change_during_the_psp_call_is_409`<br>`sandloom_billing/tests/test_operation_intents.py::test_owner_unreachable_at_the_recheck_revokes_and_expires` |
| G-BILLING-03 | The F-route authorization is serializable, not linearizable: a tenancy change committed before the final re-read is refused, one committed between the final re-read and the issue COMMIT is not seen (the acknowledged, pinned residual window). | `sandloom_billing/tests/test_operation_intents.py::test_role_change_committed_before_the_final_reread_is_refused`<br>`sandloom_billing/tests/test_operation_intents.py::test_window_role_change_after_the_final_reread_is_not_seen` |
| G-BILLING-04 | Intent lifecycle: every transition (issue, revoke, fail, reconciler) is guarded by WHERE status='pending'; issued, revoked and failed are terminal — no retry, refusal, PSP error or reconciler run changes an issued intent or replaces its session. | `sandloom_billing/tests/test_operation_intents.py::test_closing_an_issued_intent_is_a_no_op`<br>`sandloom_billing/tests/test_operation_intents.py::test_issuing_an_issued_intent_again_is_refused`<br>`sandloom_billing/tests/test_operation_intents.py::test_failed_recheck_on_retry_never_revokes_an_issued_intent`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_reconciler_never_touches_an_issued_intent` |
| G-BILLING-05 | Operation-intent route retries never hand out a bearer URL without fresh authorization: the guard stores no response (store\_response=False); an issued checkout retry re-runs the re-read (release\_check) and returns the stored URL without a PSP call (403 and no URL after authority loss, 410 past the session expiry); an issued portal retry is 409. | `sandloom_billing/tests/test_operation_intents.py::test_retry_of_an_issued_intent_returns_its_url_without_a_psp_call`<br>`sandloom_billing/tests/test_operation_intents.py::test_demoted_manager_retrying_an_issued_checkout_gets_no_url`<br>`sandloom_billing/tests/test_operation_intents.py::test_retry_of_an_issued_portal_intent_is_refused_without_a_psp_call`<br>`sandloom_billing/tests/test_operation_intents.py::test_demoted_manager_cannot_replay_a_portal_url_with_a_new_token`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_retry_of_an_issued_checkout_after_its_session_expired_is_410` |
| G-BILLING-06 | Intent ownership is enforced at application level on every path: retry lookup by actor + operation + key hash then customer and params digest; issue/close/reconcile writes by id + customer\_id (+ actor) + status; the webhook gate resolves the customer from the PSP customer and requires the intent's own session. | `sandloom_billing/tests/test_operation_intents.py::test_another_tenants_idempotency_key_is_never_reused`<br>`sandloom_billing/tests/test_operation_intents.py::test_same_key_for_another_tenant_of_the_same_actor_is_refused`<br>`sandloom_billing/tests/test_operation_intents.py::test_webhook_naming_another_customers_intent_is_not_applied`<br>`sandloom_billing/tests/test_operation_intents.py::test_checkout_completion_must_name_the_intents_own_session`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_reconciler_scopes_every_write_to_the_intents_customer` |
| G-BILLING-07 | The webhook applies a credit grant or a new subscription only for an object naming an issued intent of that customer; a completion for a revoked/pending/failed/unknown intent is not applied, audited (billing.intent.unauthorized\_completion) and flagged for an operator; objects without the marker (pre-intent sessions) apply as before. | `sandloom_billing/tests/test_operation_intents.py::test_subscription_for_a_revoked_intent_is_not_activated`<br>`sandloom_billing/tests/test_operation_intents.py::test_credit_purchase_for_a_revoked_intent_grants_nothing`<br>`sandloom_billing/tests/test_operation_intents.py::test_subscription_for_an_unknown_intent_is_not_activated`<br>`sandloom_billing/tests/test_operation_intents.py::test_session_created_before_intents_still_activates` |
| G-BILLING-08 | A subscription or credit completion tied to an intent must match the intent's price with exactly one item of quantity 1, and one intent yields at most one subscription. | `sandloom_billing/tests/test_subscription_intent_match.py::test_subscription_to_another_price_than_its_intent_is_not_activated`<br>`sandloom_billing/tests/test_subscription_intent_match.py::test_one_intent_yields_at_most_one_subscription`<br>`sandloom_billing/tests/test_subscription_intent_match.py::test_subscription_with_a_quantity_above_one_is_not_activated`<br>`sandloom_billing/tests/test_subscription_intent_match.py::test_credit_completion_for_another_price_than_its_intent_is_refused` |
| G-BILLING-09 | The periodic reconciler (beat, acks\_late) closes a pending intent older than BILLING\_INTENT\_RECONCILE\_AFTER\_MINUTES by its PSP marker: no session -\> failed, open session -\> revoked + expired, completed -\> revoked + followup\_at; lookup/expire failures are persisted and bounded by BILLING\_INTENT\_RECONCILE\_MAX\_ATTEMPTS, then left to an operator. | `sandloom_billing/tests/test_intent_reconcile.py::test_stale_pending_intent_session_is_found_by_marker_and_expired`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_stale_pending_intent_without_a_psp_session_fails`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_stale_pending_intent_paid_meanwhile_is_flagged_for_an_operator`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_psp_lookup_outage_is_bounded_then_left_for_an_operator`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_reconcile_task_is_scheduled_and_acks_late` |
| G-BILLING-10 | A failed expire compensation is counted (billing\_intent\_expire\_failed), recorded on the intent and retried by the reconciler; the refusal stands and the webhook gate remains the backstop. | `sandloom_billing/tests/test_operation_intents.py::test_a_failed_expire_is_counted_and_the_gate_remains_the_backstop`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_failed_compensation_is_persisted_and_retried`<br>`sandloom_billing/tests/test_intent_reconcile.py::test_failed_compensation_retry_is_bounded` |
| G-BILLING-11 | Only PSP-signed webhooks are processed: an invalid, missing or stale signature is rejected and the fake PSP always rejects unsigned/forged webhooks. | `sandloom_billing/tests/test_webhooks.py::TestWebhookSecurity::test_invalid_signature_rejected`<br>`sandloom_billing/tests/test_webhooks.py::TestWebhookSecurity::test_missing_signature_rejected`<br>`sandloom_billing/tests/test_webhooks.py::TestWebhookSecurity::test_stale_timestamp_rejected`<br>`sandloom_billing/tests/test_webhook_hardening.py::TestForgedWebhooks::test_forged_event_rejected_over_http`<br>`sandloom_billing/tests/test_webhook_hardening.py::TestForgedWebhooks::test_fake_without_secret_rejects_webhooks` |
| G-BILLING-12 | Webhook events are deduped by event id (a replay applies once) and unhandled event types are journaled skipped. | `sandloom_billing/tests/test_webhooks.py::TestWebhookProcessing::test_webhook_idempotent_replay`<br>`sandloom_billing/tests/test_webhooks.py::TestWebhookProcessing::test_unknown_event_type_ignored` |
| G-BILLING-13 | Webhook ownership and ordering: a subscription/invoice change applies only when its PSP customer owns the row, unknown Stripe statuses are refused, incomplete\_expired/canceled are terminal and never revived, and a stale update never rolls state back. | `sandloom_billing/tests/test_webhook_ownership.py::test_invoice_paid_of_another_customer_does_not_recover_the_subscription`<br>`sandloom_billing/tests/test_webhook_ownership.py::test_subscription_of_another_customer_is_not_taken_over`<br>`sandloom_billing/tests/test_webhook_ownership.py::test_unknown_subscription_status_is_not_applied`<br>`sandloom_billing/tests/test_webhook_ownership.py::test_incomplete_expired_is_terminal`<br>`sandloom_billing/tests/test_webhook_hardening.py::TestSubscriptionOrdering::test_stale_update_does_not_roll_back` |
| G-BILLING-14 | A credit-pack checkout grants the catalog price's credits only when the PSP customer and metadata customer\_id match, amount\_total + currency equal the price and payment\_status is paid. | `sandloom_billing/tests/test_webhook_hardening.py::TestCreditPackCheckout::test_grant_comes_from_catalog_not_metadata`<br>`sandloom_billing/tests/test_webhook_hardening.py::TestCreditPackCheckout::test_unpaid_completion_grants_nothing_until_async_success`<br>`sandloom_billing/tests/test_webhook_hardening.py::TestCheckoutAmountAndCustomerBinding::test_amount_or_currency_mismatch_grants_nothing`<br>`sandloom_billing/tests/test_webhook_hardening.py::TestCheckoutAmountAndCustomerBinding::test_metadata_customer_id_is_required` |
| G-BILLING-15 | A subscription without an intent marker is synced but grants entitlements only for a catalog price with BILLING\_GRANT\_INTENTLESS\_SUBSCRIPTIONS=true (default false); a subscription to an unknown price grants nothing (price stored NULL, audited). | `sandloom_billing/tests/test_entitlement_intentless.py::test_intentless_subscription_is_synced_but_grants_nothing_by_default`<br>`sandloom_billing/tests/test_entitlement_intentless.py::test_intentless_subscription_grants_when_the_policy_allows`<br>`sandloom_billing/tests/test_entitlement_unknown_price.py::test_change_to_an_unknown_price_withdraws_the_entitlements`<br>`sandloom_billing/tests/test_entitlement_unknown_price.py::test_new_subscription_with_an_unknown_price_grants_nothing` |
| G-BILLING-16 | A portal-driven plan / cancel\_at\_period\_end change with no issued portal intent of that customer in the attribution window is applied but audited billing.subscription.unattributed\_change; lifecycle updates are never audited and another customer's portal intent never attributes. | `sandloom_billing/tests/test_portal_attribution.py::test_cancel_at_period_end_without_a_portal_intent_is_audited_not_blocked`<br>`sandloom_billing/tests/test_portal_attribution.py::test_lifecycle_update_is_never_audited`<br>`sandloom_billing/tests/test_portal_attribution.py::test_another_customers_portal_intent_does_not_attribute` |
| G-BILLING-17 | Refunds and disputes of credit packs reverse the granted credits proportionally and cumulatively, once per delivery, only for the grant's own PSP customer; a won dispute restores them. | `sandloom_billing/tests/test_refunds_disputes.py::test_full_refund_reverses_the_granted_credits`<br>`sandloom_billing/tests/test_refunds_disputes.py::test_partial_refunds_reverse_proportionally_and_cumulatively`<br>`sandloom_billing/tests/test_refunds_disputes.py::test_a_redelivered_refund_event_is_applied_once`<br>`sandloom_billing/tests/test_refunds_disputes.py::test_refund_from_another_psp_customer_reverses_nothing`<br>`sandloom_billing/tests/test_refunds_disputes.py::test_dispute_reverses_the_credits_and_a_won_dispute_restores_them` |
| G-BILLING-18 | BILLING\_PROVIDER=stripe fails fast without STRIPE\_SECRET\_KEY + STRIPE\_WEBHOOK\_SECRET; prod refuses the fake PSP (critical startup task) unless the accepted risk fake\_psp\_in\_prod is set; both deploy profiles require an explicit BILLING\_PROVIDER. | `tests/integration/test_resilience_config.py::test_stripe_provider_requires_keys`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_prod_rejects_explicit_fake_psp_unless_opted_in`<br>`sandloom_billing/tests/test_registration_surface.py::test_fake_psp_in_prod_adds_a_critical_startup_task`<br>`sandloom/core/tests/test_deploy_profiles.py::test_billing_provider_must_be_chosen` |
| G-BILLING-19 | The checkout and portal routes are operation\_intent routes and take no request unit: each intent step is its own short sequential unit, no transaction is nested in another, and none (nor any held connection, checkout or row lock) spans the PSP call (external\_call) or the final re-read (release\_check); write admission refuses a unit opened during either (SANDLOOM-R008), including one a racing task opens while the window is entered; a read contract already running on the request's shared read connection finishes and its unit is closed before either window opens, and a contract call already running in its own provider unit finishes before either window opens (a new one is refused while it is open), whether the call is local or remote (RPC); a remote call whose provider left an attempt unanswered (timeout, transport failure, or an answer whose MAC-bound effects the caller could not merge: too large to read, no well-formed envelope, malformed effects, an unkeyed refusal - round 5) may still hold its unit, so neither window opens in that request (fail closed, 503 SANDLOOM-R008), whichever hop made the unanswered call (a provider reports its own nested unanswered calls back, caught or not, and each caller merges them). | `tests/integration/test_route_runtime.py::test_the_issue_and_issued_retry_cases_pass_and_hold_no_connection`<br>`tests/architecture/test_operation_classes.py::test_module_routes_match_their_snapshots`<br>`sandloom/core/tests/test_intent_window_race.py::test_a_unit_racing_the_window_entry_is_refused`<br>`sandloom/core/tests/test_intent_window_race.py::test_a_unit_left_open_by_the_drain_refuses_the_window`<br>`sandloom/core/tests/test_contract_read_scope.py::test_an_intent_window_waits_for_a_running_read_and_closes_its_unit`<br>`sandloom/core/tests/test_contract_read_scope.py::test_an_intent_window_never_runs_beside_a_running_provider_unit`<br>`sandloom/core/tests/test_rpc_intent_window.py::test_a_remote_command_inside_an_intent_window_is_refused`<br>`sandloom/core/tests/test_rpc_intent_window.py::test_an_intent_window_waits_for_a_running_remote_command`<br>`tests/integration/test_rpc_caller_parity.py::test_no_window_opens_while_a_timed_out_remote_command_may_run`<br>`tests/integration/test_rpc_caller_parity.py::test_a_nested_remote_timeout_keeps_the_root_in_doubt`<br>`tests/integration/test_rpc_caller_parity.py::test_an_oversized_answer_keeps_the_nested_doubt`<br>`tests/integration/test_rpc_caller_parity.py::test_an_answer_whose_effects_were_not_merged_is_in_doubt` |
| G-BILLING-20 | billing.credits.v1.spend / get\_balance hold a personal account to the end user of the call when the call context carries one (the request's subject locally, the signed caller facts over RPC, carried on by every hop: a provider's own nested calls run for the same end user, with or without a commit-time re-check context, so splitting a wrapper never turns a user call into a system call): another user's id is BillingTenantMismatchError (403); system and service callers (no end user anywhere up the chain) are unchanged. A command replay never bypasses it: the command dedupe record is scoped to the calling end user, so another user reusing a spender's operation key runs the body and gets its own 403, never the stored result (every @command, locally and over RPC); the spender's own retry still replays. | `tests/integration/test_authz_subjects.py::test_a_personal_spend_must_be_the_callers_own`<br>`tests/integration/test_durable_command_idempotency.py::test_a_replay_never_serves_another_callers_result`<br>`tests/integration/test_durable_command_idempotency.py::test_a_personal_spend_replay_is_the_spenders_only`<br>`tests/integration/test_rpc_caller_parity.py::test_the_end_user_crosses_every_hop`<br>`tests/integration/test_rpc_caller_parity.py::test_a_split_call_matches_the_local_one` |
| G-BILLING-21 | The billing API's account is chosen explicitly: without org\_id the personal account answers, also for a user in exactly one or several organizations (no single-organization inference); an organization's account needs org\_id. | `sandloom_billing/tests/test_billing_api.py::TestOrgContext::test_single_org_implies_org_context`<br>`sandloom_billing/tests/test_billing_api.py::TestOrgContext::test_multiple_orgs_without_org_id_422` |
| G-BILLING-22 | A credit spend of 0 (a free item: a zero price, a promo code) succeeds and is recorded like any spend: a ledger row with delta 0 under its operation key (a replay returns the same result, the same key with another amount is 409), the spent event, the balance unchanged; an account without a billing customer yet gets one. It succeeds whatever the balance, including a negative one left by a refund or dispute reversal (a positive spend still needs cover). A negative amount is refused (422) and a grant of 0 stays refused. | `sandloom_billing/tests/test_credits.py::TestCreditIdempotencyHardening::test_a_zero_spend_is_recorded_and_idempotent`<br>`sandloom_billing/tests/test_credits.py::TestCreditIdempotencyHardening::test_a_zero_spend_needs_no_prior_billing_customer`<br>`sandloom_billing/tests/test_credits.py::TestCreditIdempotencyHardening::test_a_zero_spend_succeeds_on_a_negative_balance`<br>`sandloom_billing/tests/test_credits.py::TestCreditIdempotencyHardening::test_amount_bounds` |

### G-CONTRACT

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-CONTRACT-01 | Every contract call, local or served over RPC, runs in the PROVIDER's own Unit of Work on the provider module's database (never the caller's session, which is restored afterwards); a read contract's transaction is always rolled back and a command contract commits its own UoW. | `sandloom/core/tests/test_contract_runtime.py::test_local_call_runs_in_providers_own_uow_on_its_own_database`<br>`sandloom/core/tests/test_contract_runtime.py::test_command_commits_and_requires_idempotency_key`<br>`sandloom/core/tests/test_service_rpc.py::test_rpc_server_runs_contract_in_provider_uow`<br>`sandloom/core/tests/test_contract_runtime.py::test_local_and_rpc_paths_are_identical` |
| G-CONTRACT-02 | A read contract that publishes an event, writes an audit/outbox row, queues an on\_commit effect, adds/modifies ORM objects or runs raw DML raises ReadContractWriteError (in its own UoW and inside a shared read scope) and its writes never persist. | `sandloom/core/tests/test_contract_runtime.py::test_read_contract_cannot_publish`<br>`tests/integration/test_read_contract_writes.py::test_read_contract_write_fails_loudly`<br>`tests/integration/test_read_contract_writes.py::test_pure_read_still_succeeds` |
| G-CONTRACT-03 | A command contract must name a mandatory idempotency-key argument at registration, and a call with an empty key is refused (ContractIdempotencyKeyMissing) before any UoW opens. | `sandloom/core/tests/test_contract_runtime.py::test_command_contract_must_name_its_idempotency_key`<br>`sandloom/core/tests/test_contract_runtime.py::test_command_commits_and_requires_idempotency_key` |
| G-CONTRACT-04 | Contract signatures take no session: a provider method with a session parameter is refused at registration (and by the architecture test), and a session passed by a caller, positionally or by keyword, is 422 ContractArgumentError before any UoW opens, locally and over RPC. | `sandloom/core/tests/test_contract_runtime.py::test_caller_session_is_an_argument_error`<br>`sandloom/core/tests/test_service_rpc.py::test_a_session_taking_provider_is_refused_at_registration`<br>`sandloom/core/tests/test_service_rpc.py::test_remote_proxy_decodes_uuid_and_refuses_a_session_argument`<br>`tests/architecture/test_contract_methods_no_session.py::test_registered_contracts_take_no_session` |
| G-CONTRACT-05 | register\_service raises ContractTypeError for a provider with a public member that is not a @contract\_method, an unannotated or non-wire-type parameter/return, or *args/*\*kwargs; a local provider exposes only its contract methods (private members and unknown names raise AttributeError). | `sandloom/core/tests/test_contract_parity.py::test_non_portable_contracts_are_rejected_at_registration`<br>`sandloom/core/tests/test_contract_parity.py::test_local_provider_exposes_only_contract_methods`<br>`tests/laws/test_wire_codec_laws.py::test_a_non_wire_annotation_is_refused_wherever_it_sits` |
| G-CONTRACT-06 | Arguments bind with Python's rules identically on the local and RPC paths: unknown/typo kwargs, missing arguments, positional-only by keyword, keyword-only positionally and arguments failing annotation validation are 422 ContractArgumentError both ways. | `sandloom/core/tests/test_contract_parity.py::test_local_and_rpc_results_and_errors_are_identical`<br>`sandloom/core/tests/test_contract_parity.py::test_rpc_type_fidelity`<br>`sandloom/core/tests/test_contract_runtime.py::test_runtime_command_binds_its_arguments_once_per_layer` |
| G-CONTRACT-07 | Results are encoded once and decoded against the return annotation on both paths, so a local caller receives exactly the types a remote one does (a pydantic model not a dict, an Enum not its value, exact Decimal, UUID, tuple/set/frozenset, non-string map keys), with a stable digest independent of the hash seed; a Contract(REF) caller receives each DTO as the class its generated stub names (the generated copy, G-CONTRACT-19) on both paths, every field set as read off the wire. | `sandloom/core/tests/test_contract_parity.py::test_rpc_type_fidelity`<br>`tests/sdk/test_sdk_contract_dtos.py::test_a_caller_receives_the_copy_equal_to_the_providers_value`<br>`sandloom/core/tests/test_contract_parity.py::test_wire_codec_round_trips_exactly`<br>`sandloom/core/tests/test_contract_parity.py::test_set_digest_is_stable_across_hash_seeds`<br>`sandloom/core/tests/test_contract_codec_v2.py::test_local_plan_equals_v1_roundtrip` |
| G-CONTRACT-08 | SANDLOOM-C012. A business module registering a contract whose pydantic model / Enum (nested included) is a class of ANOTHER module's package, or one defined in a function or **main**, fails with ContractTypeError naming the DTO, the method position and the fix; a DTO of the provider's own package (the norm: its schemas module) or of a shared package (sandloom.shared.\*, CONTRACT\_DTO\_PACKAGES) is accepted, and sandloom generate writes a copy of each own DTO into the shared DTO package. `sandloom check` (contract-dtos) reports an own-package DTO a contract signature (an @contract class's public method or a @contract\_method) names that has no generated copy. A remote caller that has not imported a DTO's class decodes it into a loaded generated copy of it, else as plain data, and never imports a module named by the peer. | `sandloom/core/tests/test_contract_dto_locality.py::test_module_contract_with_own_package_dto_is_accepted`<br>`sandloom/core/tests/test_contract_dto_locality.py::test_a_function_local_dto_is_refused`<br>`sandloom/core/tests/test_contract_dto_locality.py::test_nested_enum_of_another_loaded_module_is_refused`<br>`sandloom/core/tests/test_contract_dto_locality.py::test_remote_decode_degrades_when_the_dto_is_not_importable`<br>`sandloom/core/tests/test_contract_dto_locality.py::test_remote_decode_resolves_a_loaded_generated_copy`<br>`sandloom/core/tests/test_contract_dto_locality.py::test_shared_and_declared_dto_packages_are_accepted`<br>`tests/architecture/test_archcheck_library.py::test_contract_dto_leaks_detected`<br>rule `contract-dtos` |
| G-CONTRACT-09 | A provider HTTPException or http\_status-protocol exception reaches the caller as ContractCallError with the same status, detail, headers (e.g. Retry-After) and code locally AND over RPC; a domain 401/403 stays 401/403 over RPC, never a 503. | `sandloom/core/tests/test_contract_parity.py::test_domain_403_over_rpc_is_not_a_503`<br>`sandloom/core/tests/test_contract_parity.py::test_retry_after_header_survives_rpc`<br>`sandloom/core/tests/test_contract_parity.py::test_local_and_rpc_results_and_errors_are_identical`<br>`sandloom_billing/tests/test_contract_conformance.py::test_foreign_org_spend_is_403_not_503_over_rpc` |
| G-CONTRACT-10 | An unknown method or contract at a remote provider surfaces at the caller as 501 ServiceUnavailableInDeployment (an unknown method on a local provider is AttributeError). | `sandloom/core/tests/test_contract_parity.py::test_unknown_method_at_provider_is_501`<br>`sandloom/core/tests/test_contract_parity.py::test_local_provider_exposes_only_contract_methods` |
| G-CONTRACT-11 | An AttributeError / ValueError (or any unexpected exception) escaping provider code is a provider failure — 500 locally, 503 RemoteServiceUnavailable code=provider\_error remotely — and is never masked as the endpoint's 404 method\_not\_found or 422 binding error. | `sandloom/core/tests/test_service_rpc.py::test_rpc_errors_inside_provider_are_not_masked_as_404_or_422`<br>`sandloom/core/tests/test_contract_parity.py::test_local_and_rpc_results_and_errors_are_identical` |
| G-CONTRACT-12 | At startup every ContractRequirement in ModuleDescriptor.consumes must be provided locally or bound in SERVICE\_BINDINGS: a missing required one raises ContractBindingError (startup aborts), a missing optional one only warns, and the shipped modules declare what they consume. | `sandloom/core/tests/test_consumes_preflight.py::test_missing_required_consumed_contract_aborts_startup`<br>`sandloom/core/tests/test_consumes_preflight.py::test_missing_optional_consumed_contract_only_warns`<br>`sandloom/core/tests/test_consumes_preflight.py::test_provider_loaded_later_or_binding_satisfies`<br>`sandloom/core/tests/test_module_boots_alone.py::test_required_missing_contract_is_the_explicit_refusal` |
| G-CONTRACT-13 | Inside a request read scope LOCAL read-contract calls on one database share one contract-pool connection (READ COMMITTED, always rolled back) that never sees the caller's uncommitted writes; a failing call discards the shared transaction and nested/concurrent calls fall back to their own UoW. | `sandloom/core/tests/test_contract_read_scope.py::test_calls_in_a_scope_share_one_connection`<br>`sandloom/core/tests/test_contract_read_scope.py::test_scope_never_sees_the_callers_uncommitted_writes`<br>`sandloom/core/tests/test_contract_read_scope.py::test_failed_call_discards_the_shared_transaction`<br>`sandloom/core/tests/test_contract_read_scope.py::test_nested_and_concurrent_calls_fall_back_to_their_own_uow` |
| G-CONTRACT-14 | Identical local and RPC read calls (contract, method, bound tenant, arguments) are memoised for the request, each hit is an independent copy, the memo is keyed by the bound tenant, any command call (local or remote) clears it, and the HTTP boundary never inherits another request's memo. | `sandloom/core/tests/test_contract_read_scope.py::test_memo_serves_repeats_and_a_command_invalidates_it`<br>`sandloom/core/tests/test_contract_parity.py::test_external_changes_and_command_invalidation_have_same_read_policy`<br>`sandloom/core/tests/test_contract_read_scope.py::test_memo_hits_are_independent_copies`<br>`sandloom/core/tests/test_contract_read_scope.py::test_memo_is_keyed_by_bound_tenant` |
| G-CONTRACT-15 | In-process contract UoWs use a dedicated per-database pool (CONTRACT\_DB\_POOL\_SIZE) so a nested contract call succeeds even with DB\_POOL\_SIZE=1, and contract connections carry lock\_timeout (CONTRACT\_DB\_LOCK\_TIMEOUT\_MS) so a contract waiting on its caller's row lock fails fast instead of deadlocking. | `sandloom/core/tests/test_contract_pool.py::test_nested_contract_call_with_request_pool_size_one_succeeds`<br>`sandloom/core/tests/test_contract_pool.py::test_without_the_contract_pool_the_nested_call_starves`<br>`sandloom/core/tests/test_contract_pool.py::test_contract_waiting_on_callers_row_lock_fails_fast` |
| G-CONTRACT-16 | The typed wire codec v2 is used only for fully typed annotations and only once the provider is described and its contract hash matches the caller's accepted hash; any value not exactly fitting falls back to codec 1 with unchanged coercion/errors, and on the local path only immutable exact-type values skip the round trip (mutables are copied); codec 1 round-trips every wire value, local equals remote and codec 1 equals codec 2 when fully typed (properties), and an int beyond 64 bits crosses RPC tagged (bigint) so local still equals remote. | `sandloom/core/tests/test_contract_codec_v2.py::test_server_answers_typed_only_for_the_accepted_contract_hash`<br>`sandloom/core/tests/test_contract_codec_v2.py::test_new_client_new_server_uses_typed_codec`<br>`sandloom/core/tests/test_contract_codec_v2.py::test_local_plan_rejects_like_v1`<br>`sandloom/core/tests/test_contract_codec_v2.py::test_immutable_exact_values_pass_through_and_mutables_are_copied`<br>`tests/laws/test_wire_codec_laws.py::test_codec_1_round_trips_every_wire_value`<br>`tests/laws/test_wire_codec_laws.py::test_local_equals_remote`<br>`tests/laws/test_wire_codec_laws.py::test_codec_1_equals_codec_2_when_fully_typed`<br>`tests/laws/test_wire_codec_laws.py::test_an_int_beyond_64_bits_does_not_cross_rpc` |
| G-CONTRACT-17 | Every shipped contract (tenancy.membership.v1, billing.entitlements.v1, billing.credits.v1, billing.facts.v1, \*.claims.v1) gives identical results and errors locally and over RPC under assert\_contract\_conformance. | `sandloom_tenancy/tests/test_contract_conformance.py::test_tenancy_contracts_conform`<br>`sandloom_billing/tests/test_contract_conformance.py::test_billing_contracts_conform`<br>`sandloom_billing/tests/test_facts_contract_conformance.py::test_billing_facts_contract_conforms`<br>`sandloom_identity/tests/test_contract_conformance.py::test_identity_claims_contract_conforms` |
| G-CONTRACT-18 | Read contracts keep no idempotency record on either path, and the RPC layer adds no dedupe record of its own (commands share the contract runtime's single record). | `sandloom/core/tests/test_contract_parity.py::test_reads_persist_no_idempotency_record`<br>`sandloom/core/tests/test_contract_parity.py::test_command_dedupe_is_shared_bounded_and_size_capped`<br>`sandloom/core/tests/test_service_rpc.py::test_command_contract_is_deduped_by_the_framework_locally_and_over_rpc` |
| G-CONTRACT-19 | Contract DTOs are authored in the provider and copied for callers: sandloom generate writes a wire-shape copy of every DTO a provided contract signature names (nested ones included) into /contracts/\_dto.py, each class marked @dto\_copy with its origin, and the stub names the copy and passes its Protocol (ContractRef(name, Protocol)). A Contract(REF) caller - and Contract(REF, optional=True).client() - receives results as the stub annotates them (the copy's class), on the local path and over RPC (with or without the provider's package imported in the caller process), equal to what the provider returned; a frozen copy of immutable scalars is rebuilt without revalidation, anything else through one JSON round trip; a result already of the stub's class, or naming no model / Enum, is not converted. The provider receives its own class for a copy the caller passes, on both paths. | `tests/sdk/test_sdk_contract_dtos.py::test_a_caller_receives_the_copy_equal_to_the_providers_value`<br>`tests/sdk/test_sdk_contract_dtos.py::test_the_fast_path_copies_a_frozen_scalar_dto_without_revalidating`<br>`tests/sdk/test_sdk_contract_dtos.py::test_the_stub_names_the_copies_and_passes_its_protocol`<br>`tests/sdk/test_sdk_contract_dtos.py::test_every_copied_class_is_marked_and_carries_the_wire_shape`<br>`sandloom/core/tests/test_contract_dto_locality.py::test_remote_decode_resolves_a_loaded_generated_copy`<br>`sandloom_identity/tests/test_contact_contract.py::test_get_contact_and_get_contacts` |

### G-COMPAT

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-COMPAT-01 | Each bundled module commits contracts.snapshot.json equal to the rendered describe documents of its registered contracts; a stale or missing snapshot fails the suite. For L1 modules, sandloom generate compares every generated file with the committed one before writing anything (SANDLOOM-T004 contracts, T005 events, T007 stubs, C012 DTO copies, T001 a route that lost its authentication); sandloom test, dev and --ci alike, only checks and fails on a stale file, listing it under "generated files are stale: run `sandloom generate`". | `sandloom/core/tests/test_contract_compat.py::test_committed_module_snapshots_match_registered_contracts`<br>`tests/sdk/test_sdk_generated.py::test_apply_writes_in_dev_and_fails_stale_in_ci`<br>`tests/sdk/test_sdk_generated.py::test_a_stale_file_asks_for_sandloom_generate_and_is_left_alone`<br>`sandloom/core/tests/test_run_tests_cli.py::test_sandloom_test_never_writes_a_generated_file`<br>`tests/sdk/test_sdk_generated.py::test_stub_compatibility_is_additive`<br>`tests/sdk/test_sdk_generated.py::test_a_route_that_loses_its_authentication_is_incompatible`<br>`tests/sdk/test_sdk_generated.py::test_a_generated_stub_is_ruff_formatted` |
| G-COMPAT-02 | The compatibility comparison reports as breaking a removed method/parameter, a newly required parameter, a changed position/kind/idempotency key and a narrowed parameter or incompatible result type, and passes additive changes (new methods, optional parameters/fields, widened parameter types). | `sandloom/core/tests/test_contract_compat.py::test_additive_changes_are_compatible`<br>`sandloom/core/tests/test_contract_compat.py::test_breaking_changes_are_reported_precisely`<br>`sandloom/core/tests/test_contract_compat.py::test_type_and_order_changes_are_reported`<br>`sandloom/core/tests/test_contract_compat.py::test_narrowing_a_parameter_type_is_breaking` |
| G-COMPAT-03 | `sandloom contracts check --against <snapshot>` exits 0 on additive changes and 1 on breaking ones (default --as provider), and with --as consumer reports methods the released provider lacks. | `sandloom/core/tests/test_contract_compat.py::test_cli_check_against_snapshot`<br>**gap (S)**: No test drives `sandloom contracts check --against http://<provider>` (live describe document over the internal token) or the `sandloom contracts snapshot --write/--check` CLI exit codes (only the underlying functions are tested). |
| G-COMPAT-04 | With CONTRACT\_COMPAT\_CHECK=ready (default) a caller compares every reachable remote-bound provider's live describe document with the provider module's snapshot (found in its package without importing it): an incompatible provider fails /ready with contract\_incompatible, an additive one passes. | `sandloom/core/tests/test_contract_compat.py::test_ready_fails_on_an_incompatible_provider`<br>`sandloom/core/tests/test_contract_compat.py::test_ready_passes_on_an_additive_provider`<br>`sandloom/core/tests/test_contract_compat.py::test_expectation_is_found_in_the_provider_package_without_importing_it` |
| G-COMPAT-05 | ContractRequirement(name, methods=(...)) narrows the compatibility check to the methods the consumer calls, so breaks in other methods do not fail /ready. | `sandloom/core/tests/test_contract_compat.py::test_methods_the_consumer_does_not_call_do_not_fail_ready`<br>`sandloom/core/tests/test_contract_compat.py::test_methods_narrow_the_check` |
| G-COMPAT-06 | CONTRACT\_COMPAT\_CHECK=strict aborts startup on a reachable incompatible provider, ready only logs at startup (and fails /ready), and off restores reachability-only readiness. | `sandloom/core/tests/test_contract_compat.py::test_startup_check_strict_raises_ready_only_logs`<br>`sandloom/core/tests/test_contract_compat.py::test_compat_check_off_restores_reachability_only` |
| G-COMPAT-07 | A provider describe document without model field information yields a warning (contracts\_unverified), not an incompatibility failure. | `sandloom/core/tests/test_contract_compat.py::test_model_without_field_info_is_a_warning_not_an_error`<br>**gap (S)**: The startup log for a remote binding with no snapshot at all ("unchecked", CONTRACT\_SNAPSHOT\_PATHS absent) is not asserted. |

### G-RPC

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-RPC-01 | The request key K\_req is HKDF-SHA256 over the X25519 agreement of caller and provider with a role-ordered, direction-labelled info naming both services and kids: both sides derive the same key, the reverse direction and swapped ids give different keys, and it is domain-separated from the response-binding key (committed test vectors pin the schedule). | `sandloom/core/tests/test_rpc_pair_mac.py::test_both_sides_derive_the_same_directional_pair_key`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_request_key_is_domain_separated_from_the_response_binding_key`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_swapped_ids_and_wrong_direction_key_are_refused`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_committed_vectors` |
| G-RPC-02 | Low-order / non-canonical X25519 points and an all-zero shared secret never yield a usable pair key: they are refused at startup for every configured pair and at verification for a published caller key. | `sandloom/core/tests/test_rpc_pair_mac.py::test_low_order_points_never_yield_a_predictable_key`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_all_zero_agreement_is_refused_even_if_the_library_returned_it`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_low_order_published_caller_key_is_refused_at_verification` |
| G-RPC-03 | The MAC input is a fixed-arity length-prefixed (injective) encoding of caller, caller kid, provider, provider kid, HTTP method, target, nonce, raw context bytes and body SHA-256; tampering with any one field makes the request fail verification. | `sandloom/core/tests/test_rpc_pair_mac.py::test_encoding_is_injective_on_classic_ambiguities`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_encoding_round_trips_and_never_collides_fuzz`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_mac_input_fields_all_change_the_encoding`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_each_tampered_mac_field_is_refused` |
| G-RPC-04 | The provider verifies the single canonical X-SANDLOOM-RPC-Auth header and full 32-byte tag over the raw bytes before decoding the context or body; malformed headers, non-canonical/truncated tags, a nonce that is not 128-bit lowercase hex, unknown kids and contexts with duplicate JSON keys are all 401 rpc\_auth\_invalid. | `sandloom/core/tests/test_rpc_pair_mac.py::test_malformed_headers_and_tags_are_refused`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_non_canonical_tag_encoding_is_refused`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_unknown_kids_are_refused`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_context_with_duplicate_keys_is_refused` |
| G-RPC-05 | Requests are audience-bound by construction: a request MACed for provider P never verifies at another service Q (aud must equal RPC\_SERVICE\_ID, the key names P), and a compromised peer cannot forge another caller's request to a third service. | `sandloom/core/tests/test_rpc_pair_mac.py::test_wrong_audience_is_refused`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_captured_request_replayed_to_another_service_is_refused`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_a_peer_cannot_forge_another_callers_request`<br>`tests/split_conformance/test_split_conformance.py::test_internal_rpc_requires_an_allowed_pair_mac` |
| G-RPC-06 | A request is fresh only when \|now - iat\| \<= RPC\_AUTH\_MAX\_SKEW\_SECONDS (symmetric); otherwise 401 rpc\_auth\_expired, including out-of-range iat values (never a 500), and accepted requests skewed beyond half the window are counted per caller. | `sandloom/core/tests/test_rpc_pair_mac.py::test_freshness_window_is_symmetric_and_strict`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_r5_out_of_range_issue_times_are_expired_not_500`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_clock_skew_beyond_half_the_window_is_counted` |
| G-RPC-07 | The per-process nonce replay guard, consulted only after the tag verified and the caller is authorized, refuses a repeated (caller, nonce) within the window with 401 rpc\_auth\_replayed, keeps entries until the request could no longer be fresh, never evicts a live nonce (fails closed 503 rpc\_replay\_unavailable when full, never rebuilt) and caps one caller's share of the table. | `sandloom/core/tests/test_rpc_pair_mac.py::test_captured_request_replayed_to_the_same_provider_is_refused`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_replay_guard_detects_expires_and_fails_closed_when_full`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_r3_an_unauthorized_peer_cannot_fill_the_replay_guard`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_one_caller_cannot_take_the_whole_nonce_table` |
| G-RPC-08 | With RPC\_REPLAY\_GUARD=commands (default) command calls (all calls with `all`) are also claimed by an atomic SET NX on the security cache under a collision-free key with a constant 3\*skew TTL, so a replay landing on another process/replica is refused; a cache outage fails closed in prod and open outside it. | `sandloom/core/tests/test_rpc_pair_mac.py::test_shared_nonce_layer_catches_a_replay_on_another_process`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_commands_mode_uses_the_shared_layer_for_commands_only`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_shared_nonce_layer_outage_fails_closed_in_prod_only`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_r7_shared_replay_keys_never_collide` |
| G-RPC-09 | RPC\_REPLAY\_GUARD=local is refused in production, and outside production a startup warning says the shared nonce layer fails open. | `sandloom/core/tests/test_rpc_pair_mac.py::test_local_replay_guard_is_refused_in_prod`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_startup_warns_that_the_shared_guard_fails_open_outside_prod` |
| G-RPC-10 | The provider binds the tenant only from the MAC-covered X-SANDLOOM-RPC-Context; a bare X-Tenant-Id header on an RPC request is 400 rpc\_unsigned\_tenant, and the proxy propagates the caller's bound tenant inside the MACed context. | `sandloom/core/tests/test_service_rpc.py::test_rpc_server_rebinds_tenant_only_from_verified_context`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_x_tenant_id_is_refused_on_the_mac_path`<br>`sandloom/core/tests/test_service_rpc.py::test_rpc_proxy_propagates_bound_tenant_in_the_mac_covered_context` |
| G-RPC-11 | RPC\_ALLOWED\_CALLS is deny-by-default and consulted with the verified MAC caller id: a verified caller not allowed the contract/method gets 403 rpc\_caller\_forbidden at the provider; between real processes the ops token alone opens no contract and a caller is held to its own bindings. | `sandloom/core/tests/test_rpc_allowlist.py::test_unlisted_method_is_forbidden`<br>`sandloom/core/tests/test_rpc_allowlist.py::test_unlisted_caller_is_forbidden`<br>`sandloom/core/tests/test_rpc_allowlist.py::test_allowed_caller_succeeds`<br>`tests/split_conformance/test_split_conformance.py::test_internal_rpc_requires_an_allowed_pair_mac` |
| G-RPC-12 | A caller whose own RPC\_ALLOWED\_CALLS entry does not cover every contract in its SERVICE\_BINDINGS refuses to start, and the shipped microservices profile's allowlist equals the one derived from its bindings. | `sandloom/core/tests/test_rpc_allowlist.py::test_every_bound_contract_must_be_allowed_for_the_caller`<br>`sandloom/core/tests/test_deploy_profiles.py::test_rpc_allowlist_is_derived_from_bindings` |
| G-RPC-13 | With RPC\_COMMANDS\_REQUIRE\_SIGNATURE on, a command call (and every not-yet-described method) must also carry the MAC caller's own Ed25519 signature covering the context (incl. audience and the attempt's nonce), target and body, so a leaked provider X25519 key forges reads at most, never effects: forged, foreign-signed or re-MACed commands are refused with no effect, and `all` requires signatures on reads too. | `sandloom/core/tests/test_rpc_pair_mac.py::test_kci_a_leaked_provider_key_forges_reads_but_never_commands`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_kci_a_command_signed_by_another_service_is_refused`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_kci_a_captured_signed_command_cannot_be_re_maced_under_a_new_nonce`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_all_mode_signs_and_requires_reads_too` |
| G-RPC-14 | RPC\_COMMANDS\_REQUIRE\_SIGNATURE=auto is on in prod and off in dev/test; `off` and stale (unpublished) RPC\_DH\_EXTRA\_PRIVATE\_KEYS kids are refused under multi\_tenant\_strict (stale kids warned otherwise), and a calling service without RPC\_PRIVATE\_KEY refuses to start while the requirement is on. | `sandloom/core/tests/test_rpc_pair_mac.py::test_commands_require_signature_defaults_and_startup_rule`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_strict_profile_refuses_unsigned_commands_and_stale_extra_keys`<br>`sandloom/core/tests/test_rpc_allowlist.py::test_signed_commands_need_the_callers_ed25519_key` |
| G-RPC-15 | Every SERVICE\_BINDINGS entry must pin its provider with service\_id (startup refuses otherwise), and a caller never sends a call it cannot MAC: an unpinned binding or a provider without a published X25519 key fails 503 rpc\_auth\_unavailable with nothing sent. | `sandloom/core/tests/test_rpc_allowlist.py::test_every_binding_must_pin_its_provider`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_caller_never_falls_back_without_a_pair_key`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_an_unpinned_binding_never_sends_a_call`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_every_binding_is_pinned_to_a_keyed_provider` |
| G-RPC-16 | A process with SERVICE\_BINDINGS refuses to start without its own RPC\_DH\_PRIVATE\_KEY, a provider without one refuses every RPC call (unkeyed), and a monolith with no bindings needs no RPC keys. | `sandloom/core/tests/test_rpc_allowlist.py::test_caller_with_bindings_needs_its_own_x25519_key`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_a_caller_refuses_to_start_without_its_x25519_key`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_provider_without_an_x25519_key_refuses_unkeyed`<br>`sandloom/core/tests/test_rpc_allowlist.py::test_a_monolith_needs_no_rpc_keys` |
| G-RPC-17 | Boot refuses unusable RPC key configurations: own X25519 key missing or not matching its published half, an Ed25519 private key not matching its published key, non-X25519 / non-Ed25519 keys in the maps, duplicate published X25519 keys, an underivable pair, and an X25519 key equal to the Ed25519 signing seed. | `sandloom/core/tests/test_rpc_pair_mac.py::test_startup_refuses_unusable_configurations`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_startup_refuses_a_dh_key_that_is_the_signing_seed`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_x25519_key_configuration_is_validated`<br>`sandloom/core/tests/test_rpc_allowlist.py::test_private_key_must_be_the_published_one` |
| G-RPC-18 | Every provider-side authentication/authorization rejection (rpc\_auth\_*, rpc\_identity\_*, rpc\_context\_\*, rpc\_caller\_forbidden, rpc\_replay\_unavailable) and every unkeyed error answer surfaces at the caller as 503 RemoteServiceUnavailable carrying the provider's code, never as the provider's 401/403. | `sandloom/core/tests/test_rpc_pair_mac.py::test_auth_failures_are_503_at_the_caller_with_a_stable_code`<br>`sandloom/core/tests/test_rpc_allowlist.py::test_proxy_of_an_unlisted_caller_gets_503` |
| G-RPC-19 | A caller accepts a response only when it echoes the attempt's nonce and carries a valid v2 pair-keyed MAC from the pinned service\_id; stale, swapped, wrong-request-id, unsolicited, unbound, unkeyed, forged or other-service responses are rejected (never returned as a result, even under concurrency with random faults). | `sandloom/core/tests/test_rpc_response_binding.py::test_peer_returning_a_stale_response_of_another_call`<br>`sandloom/core/tests/test_rpc_response_binding.py::test_an_unbound_reply_is_rejected`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_a_forged_response_without_the_pair_key_is_rejected`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_pinned_to_another_service_is_refused` |
| G-RPC-20 | The response MAC covers the request's own X-SANDLOOM-RPC-Auth (and command signature), and a request carrying any auth/nonce/context/signature header twice is refused and never bound, so a provider's answer to a replayed or re-labelled request never verifies for another request. | `sandloom/core/tests/test_rpc_pair_mac.py::test_r1_a_response_cannot_be_rebound_to_another_request`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_r1_request_auth_covers_the_request_mac`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_the_mac_covers_the_request_auth` |
| G-RPC-21 | Responses are keyed with an HKDF pair key over each service's DEDICATED X25519 key (never its Ed25519 signing key, no custom curve code); the provider keys call, describe and error responses only for a caller whose request MAC verified, and answers others (e.g. the 413 body cap) with the nonce alone, unkeyed; unverified callers never churn the key cache. | `sandloom/core/tests/test_rpc_keyed_binding.py::test_no_custom_curve_code_and_no_signing_key_reuse`<br>`sandloom/core/tests/test_rpc_response_binding.py::test_provider_binds_call_describe_and_error_responses`<br>`sandloom/core/tests/test_rpc_response_binding.py::test_the_body_cap_413_echoes_the_nonce_unkeyed`<br>`sandloom/core/tests/test_rpc_binding_key_caches.py::test_unverified_or_unknown_callers_never_churn_the_pair_key_cache` |
| G-RPC-22 | A response-binding rejection (other than an unkeyed refusal) discards the whole client connection pool and is a retryable transport error; a peer that always answers another call yields 503, never a result. | `sandloom/core/tests/test_rpc_response_binding.py::test_a_rejected_response_discards_the_connection_pool`<br>`sandloom/core/tests/test_rpc_response_binding.py::test_a_peer_that_always_answers_another_call_is_503_never_a_result`<br>`sandloom/core/tests/test_rpc_response_binding.py::test_concurrency_64_with_random_faults_never_returns_another_result` |
| G-RPC-23 | The aiohttp and httpx transports are behaviourally identical: both raise httpx's exception classes, pass the same fault-injection, pair-MAC and contract-conformance suites, and send pair-MACed requests with the tenant context. | `sandloom/core/tests/test_rpc_transports.py::test_request_on_the_wire_is_pair_maced_with_tenant_context`<br>`sandloom/core/tests/test_contract_parity.py::test_local_and_rpc_results_and_errors_are_identical`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_both_transports_authenticate_and_refuse_raw_replays`<br>`sandloom/core/tests/test_rpc_response_binding.py::test_httpx_transport_error_class_is_retryable` |
| G-RPC-24 | A response larger than RPC\_MAX\_RESPONSE\_BYTES is 502 RemoteServiceBadResponse and is never retried. | `sandloom/core/tests/test_rpc_transports.py::test_oversized_response_is_502_and_not_retried` |
| G-RPC-25 | The proxy retries a transport failure exactly once under a fresh nonce and tag (never seen as a replay; a signed command is re-signed and executes once), a second failure or an unreachable peer is 503, and a timeout is 503 documenting that the call may have committed. | `sandloom/core/tests/test_service_rpc.py::test_rpc_proxy_retries_transport_errors_under_a_fresh_nonce`<br>`sandloom/core/tests/test_rpc_transports.py::test_peer_closing_mid_response_twice_is_503`<br>`sandloom/core/tests/test_service_rpc.py::test_rpc_timeout_is_503_and_documents_maybe_committed`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_a_signed_command_survives_the_transport_retry` |
| G-RPC-26 | A failing aiohttp internals probe or loopback self-test (unsolicited bytes on an idle connection) warns once, increments sandloom\_rpc\_transport\_fallback\_total and rebuilds the shared client on httpx, which serves calls; the pyproject pin equals VERIFIED\_AIOHTTP\_RANGE. | `sandloom/core/tests/test_rpc_transport_compat.py::test_a_failing_probe_warns_once_counts_and_falls_back_to_httpx`<br>`sandloom/core/tests/test_rpc_transport_compat.py::test_startup_probe_failure_rebuilds_the_shared_client_on_httpx`<br>`sandloom/core/tests/test_rpc_transport_compat.py::test_a_guard_that_no_longer_engages_is_caught_by_the_self_test`<br>`sandloom/core/tests/test_rpc_transport_compat.py::test_pyproject_pins_exactly_the_verified_range` |
| G-RPC-27 | A pooled connection returns to the pool only after a complete keep-alive response: a timeout, cancellation, parse error or unsolicited byte closes it, so a half-read or stray response never reaches the next call. | `sandloom/core/tests/test_rpc_transports.py::test_unsolicited_bytes_never_reach_the_next_call`<br>`sandloom/core/tests/test_rpc_transports.py::test_cancellation_mid_call_discards_the_connection`<br>`sandloom/core/tests/test_rpc_transports.py::test_slow_peer_times_out_and_discards_the_connection`<br>`sandloom/core/tests/test_rpc_response_binding.py::test_cancellation_mid_read_never_leaks_the_late_answer` |
| G-RPC-28 | No downgrade: an Ed25519-only request is 401 rpc\_auth\_required, a request with only X-Internal-Token or no credential gets the hidden 404 (INTERNAL\_ENDPOINTS\_OPEN\_IN\_DEV never opens /\_sandloom/rpc), a bad MAC never falls back to a valid signature riding along, and a signed command stripped of its MAC is refused. | `sandloom/core/tests/test_rpc_pair_mac.py::test_downgrades_are_refused`<br>`sandloom/core/tests/test_service_rpc.py::test_rpc_server_hides_itself_from_a_request_without_credentials`<br>`sandloom/core/tests/test_contract_parity.py::test_open_dev_mode_does_not_open_rpc`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_r2_a_signed_command_cannot_be_stripped_of_its_mac` |
| G-RPC-29 | The removed settings RPC\_AUTH\_MODE and RPC\_RESPONSE\_BINDING (like every config\_tiers.REMOVED entry) fail startup unless set to their inert value (service\_mac / strict), so no token or unkeyed mode can be re-enabled. | `sandloom/core/tests/test_config_tiers.py::test_removed_setting_fails_unless_inert` |
| G-RPC-30 | Key rotation is per service and fails no call at any step: a provider accepts its current kid and RPC\_DH\_EXTRA\_PRIVATE\_KEYS kids (an unpublished extra kid is accepted but flagged), a dropped kid is refused, caller rotation needs no provider change, and the addressed kid is RPC\_SERVICE\_DH\_CURRENT\_KIDS or deterministic regardless of JSON order (an unpublished current kid is refused). | `sandloom/core/tests/test_rpc_pair_mac.py::test_provider_rotation_has_no_failed_call_at_any_step`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_a_request_to_a_dropped_provider_kid_is_refused`<br>`sandloom/core/tests/test_rpc_keyed_binding.py::test_caller_key_rotation`<br>`sandloom/core/tests/test_security_review_regressions.py::test_pair_mac_key_selection_does_not_depend_on_json_order` |
| G-RPC-31 | RPC rejections are logged with ids and kids only (never a key, tag or derived secret), rate-limited per reason and caller, and use a fixed, unique, declared reason vocabulary. | `sandloom/core/tests/test_rpc_pair_mac.py::test_rejections_log_ids_only_never_key_material`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_r6_rejection_logs_are_rate_limited`<br>`sandloom/core/tests/test_rpc_binding_reasons.py::test_every_reason_verify_emits_is_declared` |
| G-RPC-32 | HEAD on the status route /\_sandloom/rpc/ is 405 (never served under a GET MAC). | `sandloom/core/tests/test_rpc_pair_mac.py::test_head_on_the_status_route_is_not_served` |
| G-RPC-33 | Describe and readiness probes are pair-MAC authenticated like calls and allowed when any method of the contract is allowed for the caller. | `sandloom/core/tests/test_rpc_allowlist.py::test_proxy_of_an_allowed_caller_calls_and_describes`<br>`sandloom/core/tests/test_split_readiness.py::test_required_probes_run_concurrently_capped_and_authenticated` |

### G-MODULE

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-MODULE-01 | A business module's production code never imports another business module (static, relative, dynamic importlib/sys.modules/getattr spellings, in the framework tree and in an app's modules package); a violation fails sandloom check and the architecture suite. | `tests/architecture/test_module_boundaries.py::test_module_has_no_forbidden_imports`<br>`tests/architecture/test_module_boundaries.py::test_analyzer_detects_violations_in_fixture_modules`<br>`tests/architecture/test_archcheck_library.py::test_app_namespace_cross_module_spellings`<br>rule `module-boundaries` |
| G-MODULE-02 | A business module may import from the kernel ONLY the public\_api whitelist (sandloom.core.di, facade, module\_api, descriptors, orm, projections) plus sandloom.shared. *and its own package; any other sandloom.core.* import (settings, drivers, \_internal, …) is a framework-imports violation. | `tests/architecture/test_archcheck_library.py::test_framework_imports_catch_from_sandloom_import_and_dynamic_aliases`<br>`tests/architecture/test_archcheck_library.py::test_framework_manifest_passes_every_rule`<br>`sandloom/core/tests/test_architecture.py::test_architecture_checker_runs_without_violations`<br>rule `framework-imports` |
| G-MODULE-03 | The underscore submodules of the public packages (sandloom.core.module\_api.\_*, sandloom.core.facade.\_*) are forbidden to business modules via architecture\_rules.yml forbidden\_patterns; modules import the package itself. | `tests/architecture/test_archcheck_library.py::test_framework_manifest_passes_every_rule`<br>rule `framework-imports`<br>**gap (S)**: no test proves a module importing sandloom.core.module\_api.\_service\_registry or sandloom.core.facade.\_state is flagged (only that the tree is clean) |
| G-MODULE-04 | Module code cannot reach kernel handles: .event\_bus, .infra, .service\_registry, request.app.state.core (incl. reflective getattr spellings) are rejected; only the public verbs (publish\_event, register\_event\_consumer, cache(), capability(), get\_service()) are allowed. | `tests/architecture/test_no_kernel_handle_access.py::test_module_has_no_kernel_handle_access`<br>`tests/architecture/test_no_kernel_handle_access.py::test_analyzer_detects_kernel_handles`<br>`tests/architecture/test_no_kernel_handle_access.py::test_analyzer_detects_reflective_kernel_handles`<br>`tests/architecture/test_archcheck_library.py::test_kernel_handles_rule_runs_in_sandloom_check`<br>rule `kernel-handles` |
| G-MODULE-05 | Business modules never call a capability's raw\_client() / raw\_backend() escape hatch (direct or reflective); a call fails sandloom check. | `tests/architecture/test_no_raw_provider_usage.py::test_module_has_no_raw_provider_usage`<br>`tests/architecture/test_no_raw_provider_usage.py::test_analyzer_detects_raw_call`<br>`tests/architecture/test_no_raw_provider_usage.py::test_analyzer_detects_reflective_raw_access`<br>rule `raw-provider` |
| G-MODULE-06 | Business modules never import a backend SDK (boto3, motor, pymongo, neo4j, temporalio, kafka), the legacy globals (get\_db/get\_event\_bus from sandloom.core, get\_cache from sandloom.core.di) or the infra-internal modules (sandloom.core.\_internal, eventing.event\_bus, storage, drivers, sql\_alchemy.models). | `tests/architecture/test_no_legacy_infra_imports.py::test_module_has_no_legacy_infra_imports`<br>`tests/architecture/test_no_legacy_infra_imports.py::test_analyzer_detects_legacy_infra_import`<br>rule `infra-imports` |
| G-MODULE-07 | Business-module code never calls db.commit() / db.rollback(); the Unit of Work owns the transaction boundary and a call fails sandloom check. | `tests/architecture/test_no_uow_boundary_breaks.py::test_module_does_not_break_uow_boundary`<br>`tests/architecture/test_no_uow_boundary_breaks.py::test_analyzer_detects_boundary_break`<br>rule `uow-boundary` |
| G-MODULE-08 | A best-effort downgrade in module code (publish\_event/record\_audit with required=False, incl. aliased calls and \*\*kwargs splats) needs a '\# sandloom: best-effort ' marker on the call, else sandloom check fails; guaranteed calls are never flagged. | `tests/architecture/test_best_effort_effects_rule.py::test_fixture_violations_are_flagged_and_marked_calls_pass`<br>`tests/architecture/test_best_effort_effects_rule.py::test_aliased_effects_and_kwargs_splat_are_flagged`<br>`tests/architecture/test_best_effort_effects_rule.py::test_guaranteed_calls_are_never_flagged`<br>`tests/architecture/test_best_effort_effects_rule.py::test_rule_runs_in_sandloom_check`<br>rule `best-effort-effects` |
| G-MODULE-09 | get\_core() is async: an unawaited direct call in module code is flagged by sandloom check, while awaited calls and Depends(get\_core) pass. | `tests/architecture/test_get_core_await_rule.py::test_unawaited_direct_calls_are_flagged`<br>`tests/architecture/test_get_core_await_rule.py::test_awaited_calls_and_depends_are_fine`<br>`tests/architecture/test_get_core_await_rule.py::test_core_from_request_equals_awaited_get_core`<br>rule `get-core-await` |
| G-MODULE-10 | HTTP endpoints (router decorators) of a business module live only in its own api/ layer, judged relative to the module root (an api/ directory above the module does not exempt it). | `tests/architecture/test_no_endpoints_outside_api_layer.py::test_no_endpoints_outside_api_layer`<br>`tests/architecture/test_no_endpoints_outside_api_layer.py::test_analyzer_flags_endpoint_outside_api_layer`<br>`tests/architecture/test_no_endpoints_outside_api_layer.py::test_api_directory_above_the_module_root_does_not_exempt_it`<br>`tests/architecture/test_no_endpoints_outside_api_layer.py::test_sandloom_check_judges_api_layer_relative_to_module`<br>rule `endpoints-outside-api` |
| G-MODULE-11 | API endpoints of a business module do not leak ORM models (no ORM types in endpoint signatures/returns; they return pydantic DTOs), and the legacy ORM\_LEAKAGE\_BASELINE is empty and may only shrink. | `tests/architecture/test_api_no_orm_leakage.py::test_api_no_orm_leakage_in_endpoints`<br>`tests/architecture/test_api_no_orm_leakage.py::test_analyzer_flags_orm_leakage_in_endpoints`<br>`tests/architecture/test_archcheck_library.py::test_orm_leakage_with_app_model_roots`<br>`tests/architecture/test_ledger_static_pins.py::test_orm_leakage_baseline_stays_empty`<br>rule `orm-leakage` |
| G-MODULE-12 | A business module's schemas/ (package or single schemas.py) never import SQLAlchemy or the module's models (absolute, relative or any models spelling). | `tests/architecture/test_schemas_no_orm_imports.py::test_business_module_schemas_do_not_import_orm`<br>`tests/architecture/test_schemas_no_orm_imports.py::test_analyzer_detects_every_models_spelling`<br>`tests/architecture/test_schemas_no_orm_imports.py::test_single_file_schemas_module_is_checked`<br>`tests/architecture/test_archcheck_library.py::test_relative_model_imports_count_in_schemas`<br>rule `schema-orm-imports` |
| G-MODULE-13 | Module tests import the framework only through a whitelist (public\_api + shared\_api + test\_api sandloom.core.testing/sandloom.core.contract\_testing + own package); any kernel internal or other business module import fails, and every test allowlist entry must still match a real import (stale entries fail). | `tests/architecture/test_archcheck_library.py::test_test_imports_is_a_whitelist`<br>`tests/architecture/test_archcheck_library.py::test_test_imports_whitelist_runs_in_sandloom_check`<br>`tests/architecture/test_archcheck_library.py::test_test_imports_rule_honours_the_allowlist`<br>`tests/architecture/test_archcheck_library.py::test_legacy_test_import_baseline_has_no_stale_entries`<br>rule `test-imports` |
| G-MODULE-14 | register = define\_module(**name**, ...) mounts .api's router, registers .events.EVENT\_SCHEMAS and returns the descriptor; re-registering an identical schema is a no-op while a breaking schema change fails the load; inference refuses **main**; a business module named like a conventional submodule (tasks) owns its declarations, and Saga\[T\](...) records its constructing module. | `sandloom/core/tests/test_define_module.py::test_conventions_mount_router_and_register_schemas`<br>`sandloom/core/tests/test_define_module.py::test_identical_schema_reregistration_is_idempotent_breaking_still_raises`<br>`sandloom/core/tests/test_define_module.py::test_inference_refuses_main`<br>`tests/sdk/test_sdk_discovery.py::test_plain_routers_keep_todays_rule_and_module_routers_join`<br>`tests/sdk/test_sdk_discovery.py::test_a_declaration_outside_the_conventional_submodules_is_refused`<br>`tests/sdk/test_sdk_discovery.py::test_a_refused_module_registers_nothing`<br>`tests/sdk/test_sdk_discovery.py::test_events_override_skips_producer_schemas`<br>`tests/sdk/test_sdk_discovery.py::test_a_module_named_like_a_submodule_owns_its_declarations`<br>`tests/sdk/test_sdk_discovery.py::test_a_subscripted_saga_records_its_constructing_module` |
| G-MODULE-15 | Every bundled module (and every example app's module) boots ALONE through create\_app + lifespan; only the documented ContractBindingError for required consumed contracts may refuse, and any startup/registration coupling or error logged at startup fails the boot. | `sandloom_identity/tests/test_module_wiring.py::test_module_boots_alone`<br>`sandloom_tenancy/tests/test_module_wiring.py::test_module_boots_alone`<br>`sandloom_billing/tests/test_module_wiring.py::test_module_boots_alone`<br>`sandloom/core/tests/test_module_boots_alone.py::test_required_missing_contract_is_the_explicit_refusal`<br>`sandloom/core/tests/test_module_boots_alone.py::test_startup_coupling_fails_the_boot`<br>`sandloom/core/tests/test_module_boots_alone.py::test_registration_coupling_fails_the_boot`<br>`examples/linkly/tests/test_modules_boot_alone.py::test_module_boots_alone`<br>`examples/projecthub/tests/test_modules_boot_alone.py::test_module_boots_alone`<br>`examples/helpdesk/tests/test_modules_boot_alone.py::test_module_boots_alone` |
| G-MODULE-16 | Module tables never go in the core migration chain (infra-only: outbox/audit/consumer\_inbox/contract\_command\_log); each module's tables live in exactly one module chain and the chains are disjoint. | `tests/integration/test_migrations_per_chain.py::test_core_chain_owns_only_infra_tables`<br>`tests/integration/test_migrations_per_chain.py::test_chains_are_disjoint_and_cover_all_orm_tables`<br>`tests/integration/test_migrations_per_chain.py::test_every_chain_has_a_baseline` |
| G-MODULE-17 | Modules reference other modules' entities by plain UUID — no cross-module foreign keys. | **gap (M)**: convention only: no sandloom check rule or test rejects a ForeignKey from one module's table to another module's table prefix |
| G-MODULE-18 | A generated module (sandloom add module) carries no framework plumbing: conftest absent or \<= 3 lines (framework tree: one-line fixtures import), migrations/env.py \<= 3 lines (run\_module\_migrations(**file**)), **init**.py \<= 8 non-blank lines with a declarative define\_module register. | `sandloom/core/tests/test_module_generation.py::test_add_module_boilerplate_is_minimal`<br>`sandloom/core/tests/test_module_generation.py::test_add_module_register_is_declarative` |
| G-MODULE-19 | An application derives its module list: sandloom.sdk.app.app\_modules(".modules") finds, statically, every subpackage whose **init** defines register; sandloom new passes it to create\_app as the default (SANDLOOM\_MODULES selects a subset). | `sandloom/core/tests/test_project.py::test_minimal_app_loads_only_its_module`<br>`sandloom/core/tests/test_project.py::test_with_batteries_orders_modules` |

### G-KERNEL

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-KERNEL-01 | sandloom.core is layered (tooling \> composition \> contracts&amp;RPC \> auth&amp;HTTP \> drivers \> messaging \> persistence \> bus \> infra \> foundation \> config); every runtime import (module-level and function-local, only TYPE\_CHECKING excluded) goes to the same or a lower layer, and the contract is exhaustive so a new sandloom.core child fails until placed. | `tests/architecture/test_kernel_import_contracts.py::test_kernel_import_contracts_are_kept`<br>import contract `kernel-layers` |
| G-KERNEL-02 | The kernel has no import cycle at any depth (packages and modules, siblings of one layer included, sub-packages like unit\_of\_work. */ facade.*), counting function-local imports. | `tests/architecture/test_kernel_import_contracts.py::test_kernel_import_contracts_are_kept`<br>import contract `kernel-acyclic` |
| G-KERNEL-03 | Configuration (sandloom.core.config: settings, tiers, accepted risks) is a leaf: it imports no other kernel package. | `tests/architecture/test_kernel_import_contracts.py::test_kernel_import_contracts_are_kept`<br>import contract `kernel-config-leaf` |
| G-KERNEL-04 | No import contract has ignore\_imports; should one ever appear it must carry a written '; reason:' line (refused otherwise) and import-linter fails an unmatched entry. | `tests/architecture/test_kernel_import_contracts.py::test_every_ignored_import_has_a_written_reason`<br>`tests/architecture/test_ledger_static_pins.py::test_import_contracts_have_no_ignore_imports`<br>import contract `kernel-layers`<br>import contract `kernel-acyclic`<br>import contract `kernel-config-leaf` |
| G-KERNEL-05 | Every kernel function-local import (\# noqa: PLC0415) states its category '- optional: ' or '- cold: '; a cycle break is not a category and no kernel file is exempt. | `tests/architecture/test_lint_suppression_ratchet.py::test_kernel_function_local_imports_state_their_category` |
| G-KERNEL-06 | Lint-suppression counts (noqa per rule) only go down, config-level ignores of ratcheted rules are pinned, and blind-except / bandit suppressions carry a reason. | `tests/architecture/test_lint_suppression_ratchet.py::test_noqa_counts_only_go_down`<br>`tests/architecture/test_lint_suppression_ratchet.py::test_config_level_ignores_of_ratcheted_rules_are_pinned`<br>`tests/architecture/test_lint_suppression_ratchet.py::test_blind_except_and_bandit_suppressions_carry_a_reason`<br>`tests/architecture/test_lint_suppression_ratchet.py::test_marker_scanner_reads_comments_only` |
| G-KERNEL-07 | The paradigm-agnostic kernel imports no SQLAlchemy at runtime (TYPE\_CHECKING exempt); only the allowlisted relational surface (orm, tenant, di, sql\_alchemy/, projections/upsert, migrations/, drivers) may, and eventing never imports the relational implementation. | `tests/architecture/test_relational_paradigm_boundary.py::test_kernel_has_no_runtime_sqlalchemy_import`<br>`tests/architecture/test_relational_paradigm_boundary.py::test_allowlist_entries_are_relational`<br>`tests/architecture/test_relational_paradigm_boundary.py::test_analyzer_ignores_typing_only_and_flags_runtime`<br>`tests/architecture/test_relational_paradigm_boundary.py::test_eventing_never_imports_the_relational_implementation` |
| G-KERNEL-08 | Lower kernel modules never reach UP to the aggregators: no un-baselined runtime import of sandloom.core.facade or sandloom.core.di from inside the kernel, and the facade does not import the concrete RPC mechanism (ratchet baselines may only shrink). | `tests/architecture/test_core_kernel_layering.py::test_no_unbaselined_runtime_facade_import`<br>`tests/architecture/test_no_legacy_infra_imports.py::test_kernel_has_no_unbaselined_upward_di_import`<br>`tests/architecture/test_core_kernel_layering.py::test_facade_does_not_import_concrete_rpc` |
| G-KERNEL-09 | The audited cross-tenant system channel (sandloom.core.drivers.system\_db) is unreachable from business modules: it is a forbidden infra module, absent from the public core API, and the database provider exposes no system engine. | `tests/architecture/test_system_channel_unreachable.py::test_system_channel_modules_are_forbidden_infra_modules`<br>`tests/architecture/test_system_channel_unreachable.py::test_system_channel_not_in_public_core_api`<br>`tests/architecture/test_system_channel_unreachable.py::test_module_import_of_system_channel_is_flagged`<br>`tests/architecture/test_system_channel_unreachable.py::test_provider_does_not_expose_system_engine`<br>rule `infra-imports` |
| G-KERNEL-10 | sandloom.shared stays module-agnostic: it never imports a business module (absolute or relative), so it cannot become a transitive coupling channel. | `tests/architecture/test_shared_purity.py::test_shared_does_not_import_business_modules` |
| G-KERNEL-11 | The enforced public surface (the public\_api modules of architecture\_rules.yml, including the sandloom.sdk modules of Public API 1.0, + sandloom.shared.\* and the test surfaces) is snapshotted in public\_api\_baseline.json and a backward-incompatible change fails the build. | `sandloom/core/tests/test_review_tooling_checks.py::test_api_gate_preserves_generic_type_arguments_and_default_values`<br>`tests/architecture/test_sdk_reference.py::test_every_public_module_carries_a_current_reference_block` |
| G-KERNEL-12 | The framework carries no deprecated API: its own suite treats DeprecationWarning from sandloom as an error (pyproject filterwarnings 'error::DeprecationWarning:sandloom'), and generated apps set error::DeprecationWarning. | `sandloom/core/tests/test_project.py::test_generated_app_fails_on_deprecation_warnings`<br>`sandloom/core/tests/test_outbox_relay_backoff.py::test_start_relay_background_never_uses_deprecated_get_event_loop`<br>`tests/architecture/test_ledger_static_pins.py::test_framework_deprecations_fail_its_suite` |
| G-KERNEL-13 | The kernel names no business module: sandloom.core settings carry no default module list, the standard root sandloom.main composes whatever module packages are installed (their sandloom.modules entry points), and the kernel factory alone builds a working app. | `sandloom/core/tests/test_generated_project_packaging.py::test_minimal_app_uses_the_kernel_factory`<br>`sandloom/core/tests/test_module_names.py::test_the_kernel_names_no_module` |

### G-INFRA

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-INFRA-01 | A backend constructor (create\_async\_engine, redis.from\_url, Celery(...), Storage(...)) appears only inside sandloom.core.drivers/\*\*; concrete drivers are imported only by the composition layer (app\_factory, \_internal.db\_provider), and the public DI surface / ops health imports no concrete driver. | `tests/architecture/test_infra_constructed_only_in_drivers.py::test_no_backend_construction_outside_drivers`<br>`tests/architecture/test_infra_constructed_only_in_drivers.py::test_analyzer_detects_construction`<br>`tests/architecture/test_core_kernel_layering.py::test_concrete_drivers_imported_only_in_composition_layer`<br>`tests/architecture/test_no_legacy_infra_imports.py::test_di_public_surface_imports_no_concrete_driver`<br>`tests/architecture/test_ops_health_uses_infra_registry.py::test_ops_health_does_not_use_driver_default_accessors` |
| G-INFRA-02 | There is no implicit default registry: cache() / notifier() / any capability accessor with no registry bound raises RuntimeError in every environment, with a hint naming create\_app, load\_composition\_core and the sandloom.core.testing harness. | `sandloom/core/tests/test_di_get_db.py::test_get_db_surfaces_missing_registry`<br>`sandloom/core/tests/test_ledger_runtime_pins.py::test_capability_surfaces_raise_without_a_bound_registry` |
| G-INFRA-03 | A driver returning a provider that does not implement its capability's contract class is refused with ProviderContractError (also for an explicit contract mismatch at resolve time). | `sandloom/core/tests/test_capabilities.py::test_wrong_provider_contract_raises`<br>`sandloom/core/tests/test_capabilities.py::test_resolve_explicit_contract_mismatch_raises` |
| G-INFRA-04 | require\_capability(..., features={...}) raises FeatureUnsupportedError when the configured driver does not advertise a requested feature, and an unknown feature name is refused. | `sandloom/core/tests/test_capabilities.py::test_resolve_unsupported_feature_raises`<br>`sandloom/core/tests/test_capabilities.py::test_resolve_unknown_feature_raises`<br>`sandloom/core/tests/test_capabilities.py::test_facade_capability_and_require_capability` |
| G-INFRA-05 | Resolving an experimental capability (document\_store, graph\_store, workflow, search, lock, secrets) without a registered driver raises ExperimentalCapabilityError (also via the facade), with the precise error kept for an unknown instance. | `sandloom/core/tests/test_extra_capabilities.py::test_experimental_without_driver_raises_experimental_error`<br>`sandloom/core/tests/test_extra_capabilities.py::test_experimental_configured_with_unregistered_driver_raises`<br>`sandloom/core/tests/test_extra_capabilities.py::test_facade_require_capability_on_experimental_raises`<br>`sandloom/core/tests/test_extra_capabilities.py::test_experimental_with_driver_but_unknown_instance_keeps_precise_error` |
| G-INFRA-06 | The loader preflights a module's CapabilityRequirement list at load without I/O: a required requirement that is unknown / unconfigured / missing a feature fails the module load, an optional one only warns. | `sandloom/core/tests/test_module_capabilities.py::test_preflight_fails_for_unsupported_feature`<br>`sandloom/core/tests/test_module_capabilities.py::test_preflight_unknown_capability_raises`<br>`sandloom/core/tests/test_module_capabilities.py::test_optional_requirement_does_not_raise`<br>`sandloom/core/tests/test_module_capabilities.py::test_preflight_passes_for_satisfiable_requirement`<br>**gap (S)**: no test asserts the preflight performs no I/O (opens no provider/connection), nor that a failed required preflight aborts create\_app for a required module |
| G-INFRA-07 | Infra configuration is validated at registry build: an unknown driver, an unknown instance, a new INFRA instance without a driver, or options failing the capability's config schema are refused with a clear error. | `sandloom/core/tests/test_infra_registry.py::test_unknown_driver_raises`<br>`sandloom/core/tests/test_infra_registry.py::test_unknown_instance_raises`<br>`sandloom/core/tests/test_capabilities.py::test_bad_options_raise_config_schema_error`<br>`tests/integration/test_resilience_config.py::test_build_infra_registry_rejects_unknown_infra_option` |
| G-INFRA-08 | The Redis cache driver's clients follow the running event loop (rebuilt on a new loop, default and named instances alike; an injected client is never replaced), so no harness resets driver singletons. | `sandloom/core/tests/test_redis_cache_loop_affinity.py::test_default_client_is_rebuilt_on_a_new_loop`<br>`sandloom/core/tests/test_redis_cache_loop_affinity.py::test_named_instance_client_follows_the_loop`<br>`sandloom/core/tests/test_redis_cache_loop_affinity.py::test_injected_default_client_is_never_replaced` |

### G-CONFIG

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-CONFIG-01 | Every Settings field is classified in exactly one configuration tier (semantic or ops), and every semantic setting defines a value for every profile (dev / test / prod / prod+multi\_tenant\_strict). | `sandloom/core/tests/test_config_tiers.py::test_every_setting_is_in_exactly_one_tier`<br>`sandloom/core/tests/test_config_tiers.py::test_every_semantic_setting_defines_every_profile`<br>`sandloom/core/tests/test_config_tiers.py::test_profile_table_is_the_current_default_behaviour` |
| G-CONFIG-02 | The semantic tier never exceeds 16 settings, a setting that leaves it cannot silently return (baseline only shrinks), and a new semantic setting outside the baseline must reference an ADR. | `sandloom/core/tests/test_config_tiers.py::test_semantic_tier_is_ratcheted` |
| G-CONFIG-03 | A removed setting (config\_tiers.REMOVED, incl. the former **ALLOW** booleans, RPC\_AUTH\_MODE, RPC\_RESPONSE\_BINDING and AUTH\_REVOCATION\_STRICT\_PATH\_PREFIXES with inert value \[\]) set to its inert value only logs a warning; any other value fails Settings construction / startup. | `sandloom/core/tests/test_config_tiers.py::test_removed_setting_fails_unless_inert`<br>`sandloom/core/tests/test_revocation_freshness.py::test_the_freshness_knobs_are_gone`<br>`tests/architecture/test_operation_classes.py::test_strict_path_prefix_satisfies_strictness` |
| G-CONFIG-04 | In prod, JWT\_SECRET (and JWT\_PREVIOUS\_SECRETS), INTERNAL\_API\_TOKEN and METRICS\_TOKEN shorter than 32 chars or looking like placeholders are rejected at Settings validation, and METRICS\_TOKEN must differ from INTERNAL\_API\_TOKEN; dev keeps short values working. | `sandloom/core/tests/test_ops_security_hardening.py::test_prod_rejects_weak_or_placeholder_jwt_secret`<br>`sandloom/core/tests/test_ops_security_hardening.py::test_prod_validates_internal_and_metrics_tokens`<br>`sandloom/core/tests/test_auth_rotation_revocation.py::test_prod_previous_secrets_follow_jwt_secret_rules` |
| G-CONFIG-05 | A semantic setting weaker than its profile value is reported as an override (WARNING at start in prod, INFO for a dev opt-in), listed in /diagnostics/core configuration block and by sandloom doctor; a stronger choice is not reported. | `sandloom/core/tests/test_config_tiers.py::test_prod_override_is_reported_loud_and_explicit`<br>`sandloom/core/tests/test_config_tiers.py::test_stronger_choice_is_not_an_override`<br>`sandloom/core/tests/test_config_tiers.py::test_dev_opt_in_is_logged_at_info`<br>`sandloom/core/tests/test_config_tiers.py::test_doctor_lists_overrides`<br>**gap (S)**: sandloom\_config\_override\_active{setting,kind} metric export (app\_factory) is not asserted by any test. |
| G-CONFIG-06 | Framework runtime code adds no raw environment read (os.getenv / os.environ) beyond the pinned per-file baseline (the composition root, gunicorn config and bootstrap switches); every new knob is a validated Settings field. | `tests/architecture/test_ledger_static_pins.py::test_raw_environment_reads_only_go_down` |
| G-CONFIG-07 | The boot-time configuration checks (enforce\_configuration) refuse an invalid configuration identically in every entry point: the app, the Celery worker, sandloom migrate and the sandloom CLI. | `sandloom/core/tests/test_boot_configuration_checks.py::test_moved_validator_refuses_every_entry_point` |
| G-CONFIG-08 | The generated configuration-tiers reference renders every semantic setting (with its reason) and every ops setting, with the per-profile value table. | `sandloom/core/tests/test_config_tiers.py::test_generated_reference_lists_every_tier`<br>**gap (S)**: AGENTS.md and accepted\_risks error messages point at docs/reference/configuration-tiers.md, but that file does not exist in the repo (only tiers.render\_markdown() in memory); nothing checks the committed page exists / is current. |

### G-RISK

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-RISK-01 | SANDLOOM\_ACCEPTED\_RISKS accepts only registered risk ids; an unknown id fails Settings validation / startup. | `sandloom/core/tests/test_config_tiers.py::test_unknown_risk_id_fails_startup` |
| G-RISK-02 | Each accepted risk in effect in prod is logged as a WARNING 'ACCEPTED RISK' at every start, reported as an override of kind accepted\_risk, listed in /diagnostics/core configuration.accepted\_risks and flagged WARN by sandloom doctor; a prod-only risk accepted outside prod is listed with in\_effect=false and is not an override. | `sandloom/core/tests/test_config_tiers.py::test_accepted_risk_is_logged_loud_counted_and_listed`<br>**gap (S)**: Export of sandloom\_accepted\_risk\_active{risk}=1 (app\_factory) is never asserted; a test reading the metric after create\_app in prod with a risk accepted is missing. |
| G-RISK-03 | Every registered risk documents what it weakens and which check consults it, and the registry holds exactly the nine ids fake\_psp\_in\_prod, log\_notifier\_in\_prod, local\_storage\_in\_prod, volatile\_event\_bus, unversioned\_ddl\_repair, weak\_password\_hash, shared\_hs\_secret\_split, relay\_disabled, command\_dedupe\_below\_retry\_horizon. | `sandloom/core/tests/test_config_tiers.py::test_every_risk_is_documented`<br>`sandloom/core/tests/test_ledger_runtime_pins.py::test_accepted_risk_ids_are_pinned` |
| G-RISK-04 | In prod the volatile event-bus drivers (memory, redis Pub/Sub) are refused unless volatile\_event\_bus is accepted; required/durable consumers on a Pub/Sub bus are refused even with the risk, and outside prod need the risk. | `sandloom/core/tests/test_volatile_bus_guards.py::test_driver_refuses_volatile_bus_in_prod`<br>`sandloom/core/tests/test_volatile_bus_guards.py::test_memory_bus_allowed_in_dev_refused_in_prod`<br>`sandloom/core/tests/test_accepted_risks.py::test_volatile_bus_driver_admitted_in_prod_by_the_risk`<br>`sandloom/core/tests/test_accepted_risks.py::test_volatile_consumers_outside_prod_need_the_risk` |
| G-RISK-05 | OUTBOX\_RELAY\_ENABLED=off in a production app process is refused at startup (app\_factory.outbox\_relay\_problem) unless relay\_disabled is accepted, and is then reported as an override. | `sandloom/core/tests/test_accepted_risks.py::test_relay_off_in_prod_is_refused_unless_accepted`<br>`sandloom/core/tests/test_accepted_risks.py::test_relay_running_in_prod_is_fine`<br>`sandloom/core/tests/test_config_tiers.py::test_relay_off_in_prod_is_an_override_unless_the_risk_is_accepted` |
| G-RISK-06 | In prod the log notifier advertises no delivery channel features, so a CapabilityRequirement('notification', features={'email'}) fails, unless log\_notifier\_in\_prod is accepted. | `sandloom/core/tests/test_notifications.py::test_log_driver_advertises_no_delivery_features_in_prod`<br>`sandloom/core/tests/test_notifications.py::test_log_driver_delivery_features_in_prod_are_an_explicit_opt_in`<br>`sandloom/core/tests/test_accepted_risks.py::test_log_notifier_channels_in_prod_need_the_risk` |
| G-RISK-07 | BILLING\_PROVIDER=fake is refused in prod by a critical startup task unless fake\_psp\_in\_prod is accepted. | `sandloom_billing/tests/test_webhook_hardening.py::TestForgedWebhooks::test_prod_refuses_fake_psp`<br>`sandloom_billing/tests/test_webhook_hardening.py::TestForgedWebhooks::test_register_adds_critical_startup_guard_in_prod`<br>`sandloom_billing/tests/test_registration_surface.py::test_fake_psp_in_prod_adds_a_critical_startup_task` |
| G-RISK-08 | The local filesystem object store refuses to build in prod unless local\_storage\_in\_prod is accepted. | `sandloom/core/tests/test_infra_review_20261022.py::test_production_local_storage_requires_explicit_shared_volume_opt_in` |
| G-RISK-09 | Prod refuses argon2 password-hash parameters below the OWASP argon2id minimum (with a fix naming SANDLOOM\_ACCEPTED\_RISKS=\["weak\_password\_hash"\]) unless weak\_password\_hash is accepted; outside prod weak parameters only warn. | `sandloom/core/tests/test_password_hash_config.py::test_prod_refuses_weak_parameters_with_a_fix`<br>`sandloom/core/tests/test_password_hash_config.py::test_prod_accepts_presets_and_the_explicit_opt_out`<br>`sandloom/core/tests/test_password_hash_config.py::test_weak_parameters_are_allowed_outside_prod_with_a_warning` |
| G-RISK-10 | A split prod process using a shared HS JWT secret is refused unless shared\_hs\_secret\_split is accepted, and the multi\_tenant\_strict profile refuses that risk outright. | `sandloom/core/tests/test_accepted_risks.py::test_shared_hs_secret_split_in_prod_needs_the_risk`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_prod_split_deployment_refuses_a_shared_hs_secret`<br>`sandloom/core/tests/test_accepted_risks.py::test_strict_profile_refuses_the_shared_secret_risk` |
| G-RISK-11 | sandloom migrate ensure-outbox --force --apply in prod is refused (exit 1) unless unversioned\_ddl\_repair is accepted. | **gap (S)**: No test exercises the unversioned\_ddl\_repair gate in sandloom/core/cli/migrate.py (only ensure-outbox dry-run / dev apply are tested). |
| G-RISK-12 | Production startup refuses a command dedupe TTL below the retry horizon (GuaranteeWindowError naming \[command\_dedupe\_below\_retry\_horizon\]) unless that risk is accepted; dev only warns. | `sandloom/core/tests/test_guarantee_windows.py::test_production_refuses_and_names_the_risk`<br>`sandloom/core/tests/test_guarantee_windows.py::test_dev_warns` |

### G-MIGRATE

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-MIGRATE-01 | Each module owns its own Alembic chain with version table alembic\_version\_; chains are disjoint, together cover every ORM table, and reproduce the ORM schema exactly. | `tests/integration/test_migrations_per_chain.py::test_chains_are_disjoint_and_cover_all_orm_tables`<br>`tests/integration/test_migrations_per_chain.py::test_chains_reproduce_orm_schema_exactly`<br>`tests/integration/test_migrations_per_chain.py::test_every_chain_has_a_baseline` |
| G-MIGRATE-02 | A module chain is applied to exactly one database: migrations\_database if set, else the single required database requirement, else default; optional requirements never host it, and several required instances without default fail unless migrations\_database is set. | `sandloom/core/tests/test_loader_migrations_hardening.py::test_database_instances_single_target`<br>`sandloom/core/tests/test_loader_migrations_hardening.py::test_optional_database_never_hosts_chain`<br>`sandloom/core/tests/test_loader_migrations_hardening.py::test_ambiguous_databases_require_explicit_target` |
| G-MIGRATE-03 | Two modules resolving to the same migrations\_chain key fail the run / module load. | `sandloom/core/tests/test_loader_migrations_hardening.py::test_chain_collision_detected`<br>`sandloom/core/tests/test_loader_migrations_hardening.py::test_loader_rejects_colliding_chain` |
| G-MIGRATE-04 | Migration runs hold a per-database advisory lock: a concurrent run waits at most --lock-timeout seconds (must be positive) and logs the holder's pid / application\_name. | `sandloom/core/tests/test_loader_migrations_hardening.py::test_migration_lock_serialises_concurrent_runs`<br>`sandloom/core/tests/test_ops_cli.py::test_lock_wait_names_the_holder_and_honours_the_timeout`<br>`sandloom/core/tests/test_ops_cli.py::test_lock_timeout_must_be_positive` |
| G-MIGRATE-05 | The core Alembic chain creates only shared infrastructure tables (outbox\_messages, audit\_log, consumer\_inbox, contract\_command\_log, http\_idempotency\_log); module tables never go in it. | `tests/integration/test_migrations_per_chain.py::test_core_chain_owns_only_infra_tables`<br>**gap (S)**: Doc drift: AGENTS.md lists four core tables (outbox/audit/consumer\_inbox/contract\_command\_log) but the chain (and the test) also own http\_idempotency\_log. |
| G-MIGRATE-06 | sandloom migrate ensure-outbox is a dry run unless --apply, and ensure-outbox --apply is refused in prod without --force (prod uses upgrade-core). | `sandloom/core/tests/test_migrations_multidb.py::test_ensure_outbox_dry_run_is_noop`<br>**gap (S)**: The prod refusal of ensure-outbox --apply without --force (cli/migrate.py) is untested. |
| G-MIGRATE-07 | sandloom migrate extract-module is a dry run by default (writes nothing); --apply requires --i-have-stopped-writes, the fence blocks a live writer, and a same-database, non-empty target, a source behind head, or an RLS-enabled target role are refused before copying. | `sandloom/core/tests/test_module_extraction.py::test_dry_run_writes_nothing`<br>`sandloom/core/tests/test_module_extraction.py::test_apply_requires_stopped_writes`<br>`sandloom/core/tests/test_module_extraction.py::test_live_writer_blocks_the_fence`<br>`sandloom/core/tests/test_module_extraction.py::test_non_empty_target_is_refused` |
| G-MIGRATE-08 | extract-module is crash-resumable and verifiable (row counts + checksums vs manifest), --unfence rolls back only before completion (after it needs --force), --drop-source is a separate explicit cleanup, and the consumer group-owner fingerprint is transferred to the target database. | `sandloom/core/tests/test_module_extraction.py::test_extract_crash_resume_verify_and_cleanup`<br>`sandloom/core/tests/test_module_extraction.py::test_unfence_rolls_back_before_completion`<br>`sandloom/core/tests/test_module_extraction.py::test_group_owner_transfer_redis_transport`<br>`sandloom/core/tests/test_module_extraction.py::test_shared_rows_stream_idempotently_and_detect_missing_keys` |
| G-MIGRATE-09 | sandloom migrate resolves the module set through the composition root (SANDLOOM\_COMPOSITION\_ROOT) and upgrade-all exits non-zero on failure, including a database stamped at a non-head revision. | `sandloom/core/tests/test_loader_migrations_hardening.py::test_cli_uses_composition_root_module_set`<br>`sandloom/core/tests/test_loader_migrations_hardening.py::test_upgrade_all_rejects_non_head_rev`<br>`tests/integration/test_migration_revision_cli.py::test_revision_respects_composition_root` |
| G-MIGRATE-10 | Module chains are applied in a deterministic order honouring declared dependencies; a dependency cycle or a missing dependency of a required module is fatal. | `sandloom/core/tests/test_migrations_ordering.py::test_order_is_deterministic_and_respects_dependencies`<br>`sandloom/core/tests/test_migrations_ordering.py::test_cycle_is_fatal`<br>`sandloom/core/tests/test_migrations_ordering.py::test_missing_dependency_of_required_module_is_fatal` |
| G-MIGRATE-11 | Migrations derive an explicit postgresql+psycopg2:// URL from the app's asyncpg URL (to\_sync\_url, idempotent, mapping asyncpg ssl= to libpq sslmode=). | `sandloom/core/tests/test_migrations_multidb.py::test_to_sync_url_is_explicit_and_idempotent`<br>`sandloom/core/tests/test_migrations_multidb.py::test_to_sync_url_translates_asyncpg_ssl_to_sslmode` |
| G-MIGRATE-12 | sandloom migrate drift (and the sandloom test step "migration drift", SANDLOOM-T006) applies the core chain and every module chain, in the upgrade-all dependency order, to a scratch database on the default database's server (created and dropped by the check) and compares each chain with its models (each env.py compares only the module's tables); a difference fails naming the module, the operations found and the fix (sandloom migrate new  ""). The configured databases are never written. | `tests/sdk/test_sdk_generators.py::test_migration_drift_names_the_module_and_the_fix`<br>`sandloom/core/tests/test_run_tests_cli.py::test_the_plan_of_the_framework_tree` |

### G-OPS

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-OPS-01 | Celery beat schedules only while it holds a fenced cache lease: a second beat stands by and takes over on release/death, a holder that lost its token stops at once, a cache outage keeps scheduling only until a local deadline, and beat refuses to start without a CAS-capable cache. | `sandloom/core/tests/test_beat_lease.py::test_second_beat_stands_by_and_takes_over_after_release`<br>`sandloom/core/tests/test_beat_lease.py::test_a_holder_that_lost_its_token_stops_scheduling_at_once`<br>`sandloom/core/tests/test_beat_lease.py::test_cache_outage_keeps_scheduling_only_until_the_local_deadline`<br>`sandloom/core/tests/test_beat_lease.py::test_beat_refuses_to_start_without_a_cas_cache` |
| G-OPS-02 | /health and /ready answer 503 when degraded; public callers see check statuses only (details and /ready topology need X-Internal-Token) and results are cached for HEALTH\_CACHE\_TTL\_SECONDS. | `sandloom/core/tests/test_ops_security_hardening.py::test_public_health_probes_are_cached_and_topology_hidden`<br>`sandloom/core/tests/test_consumer_runtime.py::test_ready_endpoint_rejects_unavailable_required_consumer`<br>`sandloom/core/tests/test_module_subset.py::test_subset_whose_only_module_failed_is_not_ready`<br>`sandloom/core/tests/test_revocation_never_dead_lettered.py::test_ready_degrades_while_a_revocation_is_stuck_liveness_unaffected` |
| G-OPS-03 | Peers never feed /health (liveness); a down peer fails only /ready and only when its SERVICE\_BINDINGS entry is required: true, with concurrent, capped, authenticated probes. | `sandloom/core/tests/test_split_readiness.py::test_down_peer_does_not_fail_health_or_ready_by_default`<br>`sandloom/core/tests/test_split_readiness.py::test_required_peer_down_fails_ready_but_never_health`<br>`sandloom/core/tests/test_split_readiness.py::test_required_probes_run_concurrently_capped_and_authenticated` |
| G-OPS-04 | /metrics, /diagnostics/*, /contracts/* and health details are closed without INTERNAL\_API\_TOKEN (open in dev only with INTERNAL\_ENDPOINTS\_OPEN\_IN\_DEV, never in prod); METRICS\_TOKEN unlocks /metrics only. | `sandloom/core/tests/test_internal_auth.py::test_closed_in_dev_without_token_by_default`<br>`sandloom/core/tests/test_internal_auth.py::test_dev_opt_in_ignored_in_prod`<br>`sandloom/core/tests/test_internal_auth.py::test_metrics_token_unlocks_only_metrics`<br>`sandloom/core/tests/test_internal_auth.py::test_disabled_in_prod_without_token` |
| G-OPS-05 | Workers are health-checked by `python -m sandloom.core.worker healthcheck`, which fails when the heartbeat written on the worker timer is stale; every deploy profile's worker uses it and is sized from the CPU quota. | `sandloom/core/tests/test_worker.py::test_heartbeat_freshness`<br>`sandloom/core/tests/test_worker.py::test_healthcheck_cli`<br>`sandloom/core/tests/test_worker.py::test_heartbeat_step_runs_on_the_worker_timer`<br>`sandloom/core/tests/test_deploy_profiles.py::test_workers_are_sized_and_health_checked` |
| G-OPS-06 | Under gunicorn exactly one elected worker runs the outbox relay and the others are `delegated`; the leader is re-elected after a crash, TTOU retirement or SIGHUP reload, and `delegated` is refused outside a gunicorn worker. | `sandloom/core/tests/test_gunicorn_conf.py::test_gunicorn_elects_exactly_one_relay_leader`<br>`sandloom/core/tests/test_gunicorn_conf.py::test_crashed_leader_is_replaced_without_recycling_others`<br>`sandloom/core/tests/test_gunicorn_conf.py::test_relay_leader_is_reelected_when_ttou_retires_it`<br>`sandloom/core/tests/test_accepted_risks.py::test_delegated_relay_needs_a_gunicorn_worker` |
| G-OPS-07 | A test run refuses (exit 4, before any test) a test database or Redis index stamped by another owner (SANDLOOM\_TEST\_OWNER / checkout root) unless SANDLOOM\_TEST\_TAKE\_OWNERSHIP=1; flush\_owned\_redis never flushes another run's Redis. | `sandloom/core/tests/test_testing_ownership.py::test_an_unmarked_database_is_claimed_then_refused_to_others`<br>`sandloom/core/tests/test_testing_ownership.py::test_a_run_on_a_foreign_database_aborts_before_any_test`<br>`sandloom/core/tests/test_testing_ownership.py::test_flush_refuses_another_runs_redis_and_keeps_its_data`<br>`sandloom/core/tests/test_testing_ownership.py::test_takeover_is_explicit` |
| G-OPS-08 | The harness's db\_engine refuses to reset any database whose name lacks 'test', even with ENVIRONMENT=test. | `sandloom/core/tests/test_testing_plugin.py::test_refuses_a_non_test_database`<br>`sandloom/core/tests/test_review_tooling_regressions.py::test_environment_test_does_not_authorize_wiping_production` |
| G-OPS-09 | Test processes get small pools and the session fails fast before any test when the Postgres server lacks connection headroom; -n auto is capped to the server. | `sandloom/core/tests/test_testing_connections.py::test_a_run_without_room_aborts_before_any_test`<br>`sandloom/core/tests/test_testing_connections.py::test_auto_workers_are_capped_to_the_server`<br>`sandloom/core/tests/test_testing_connections.py::test_a_test_process_needs_a_small_fixed_number_of_connections` |
| G-OPS-10 | create\_app inside a test does not leak: the harness restores the active core / infra / bus / module lists after every test; every test module starts from the harness app's runtime (built before the first module, whatever ran before); what a test registers on the bound core (event schemas, services, startup tasks, declared operations, ...) is rewound after it, SQLAlchemy's class-level event flags are put back and the RPC client's description caches emptied. The test steps of sandloom test and CI run in a seeded random order (printed, replayable), and CI runs the whole framework suite serially in one shuffled process. | `sandloom/core/tests/test_testing_isolation.py::test_step_1_a_test_builds_its_own_app`<br>`sandloom/core/tests/test_testing_isolation.py::test_step_2_the_next_test_gets_the_previous_runtime_back`<br>`sandloom/core/tests/test_testing_isolation.py::test_restore_puts_back_identity_and_content`<br>`sandloom/core/tests/test_testing_isolation.py::test_a_registered_channel_is_gone_after_the_test`<br>`sandloom/core/tests/test_testing_isolation.py::test_class_flags_and_process_caches_are_put_back`<br>`sandloom/core/tests/test_testing_shuffle.py::test_a_seed_fixes_the_order_and_is_printed_for_replay`<br>`sandloom/core/tests/test_testing_shuffle.py::test_files_stay_contiguous_and_their_tests_are_shuffled`<br>`sandloom/core/tests/test_run_tests_cli.py::test_the_test_steps_run_in_a_seeded_random_order` |
| G-OPS-11 | sandloom config dump redacts secrets recursively (incl. URL credentials) and refuses --show-secrets in prod. | `sandloom/core/tests/test_ops_cli.py::test_redaction_is_recursive_and_masks_url_credentials`<br>`sandloom/core/tests/test_ops_cli.py::test_show_secrets_is_refused_in_prod`<br>`sandloom/core/tests/test_ops_cli.py::test_secret_names` |
| G-OPS-12 | sandloom security revoke-sessions / force-password-reset are dry runs (count targets, call no command) unless --apply --reason, require exactly one target, and in prod also --confirm-production. | `sandloom/core/tests/test_security_cli.py::test_dry_run_counts_targets_and_calls_no_command`<br>`sandloom/core/tests/test_security_cli.py::test_cli_requires_exactly_one_target_and_a_reason_to_apply`<br>**gap (S)**: The prod --confirm-production requirement of sandloom security is untested (only sandloom backup's guard rails test it). |
| G-OPS-13 | Logs (structured extras and free-text messages), exception descriptions and Sentry events are scrubbed of secrets/PII before emission. | `sandloom/core/tests/test_ops_security_hardening.py::test_log_extras_redacted_by_substring`<br>`sandloom/core/tests/test_ops_security_hardening.py::test_free_text_messages_are_scrubbed`<br>`sandloom/core/tests/test_ops_security_hardening.py::test_sentry_before_send_scrubs_event`<br>`sandloom/core/tests/test_ops_security_hardening.py::test_describe_exception_drops_statement_params` |
| G-OPS-14 | In both compose profiles every security/auth-relevant setting (shared env anchor: JWT\_\* except issuer-only keys, token lifetimes, INTERNAL\_API\_TOKEN, notifier/SMTP, BILLING\_DENY\_ON\_PAST\_DUE) and CACHE\_URL are identical in every process. | `sandloom/core/tests/test_deploy_profiles.py::test_security_relevant_settings_identical_across_processes`<br>`sandloom/core/tests/test_deploy_profiles.py::test_parity_keys_default_identically_in_both_profiles` |
| G-OPS-15 | The microservices gateway never routes /\_sandloom, /diagnostics or /metrics, re-resolves services through Docker DNS, and caps bodies at the app upload limit. | `sandloom/core/tests/test_deploy_profiles.py::test_gateway_never_exposes_internal_surfaces`<br>`sandloom/core/tests/test_deploy_profiles.py::test_gateway_follows_dns_and_fails_fast`<br>`sandloom/core/tests/test_deploy_profiles.py::test_gateway_body_limit_matches_app_upload_limit` |
| G-OPS-16 | Each app/worker process waits for its one-shot migrate service (sandloom migrate upgrade-all --apply, depends\_on service\_completed\_successfully) against its own database before starting. | `sandloom/core/tests/test_deploy_profiles.py::test_microservices_db_per_service_and_bindings`<br>`tests/architecture/test_ledger_static_pins.py::test_monolith_migrate_one_shot_gates_every_process` |
| G-OPS-17 | Every set of periodic tasks in a deploy profile is scheduled by exactly one beat, and modules with periodic tasks are discovered by it. | `sandloom/core/tests/test_deploy_profiles.py::test_every_periodic_task_set_has_exactly_one_beat`<br>`sandloom/core/tests/test_deploy_profiles.py::test_periodic_task_modules_are_discovered` |
| G-OPS-18 | sandloom backup verify fails when the restore fails, a chain is behind its head, or orphan rows exist, always dropping its scratch database; destructive operator paths in prod need --confirm-production. | `sandloom/core/tests/test_backup_cli.py::test_verify_fails_when_a_chain_is_behind_its_head`<br>`sandloom/core/tests/test_backup_cli.py::test_verify_fails_when_the_restore_fails`<br>`sandloom/core/tests/test_backup_cli.py::test_verify_flags_orphan_rows_of_a_broken_restore`<br>`sandloom/core/tests/test_backup_cli.py::test_operator_guard_rails` |
| G-OPS-19 | After a security-cache restore from a stale snapshot, sandloom backup reconcile-security re-derives block and revocation marks from durable records so no blocked user is re-enabled and no newer mark in the cache is lowered. | `sandloom/core/tests/test_security_recovery.py::test_stale_snapshot_after_block_does_not_reenable_the_user`<br>`sandloom/core/tests/test_security_recovery.py::test_stale_snapshot_after_unblock_keeps_the_user_blocked_unless_lifted`<br>`sandloom/core/tests/test_security_recovery.py::test_lost_revocation_is_reapplied_from_the_outbox`<br>`sandloom/core/tests/test_security_recovery.py::test_a_newer_mark_in_the_cache_is_never_lowered` |
| G-OPS-20 | The production image (deploy/docker/Dockerfile) builds from the locked uv.lock and runs as a non-root user. | `tests/architecture/test_ledger_static_pins.py::test_backend_image_is_locked_and_non_root` |
| G-OPS-21 | Every module listed explicitly in SANDLOOM\_MODULES is required (load failure aborts startup) unless MODULE\_REQUIRED names a subset, and every deploy process lists its modules as required. | `sandloom/core/tests/test_split_readiness.py::test_explicitly_listed_modules_are_required_by_default`<br>`sandloom/core/tests/test_split_readiness.py::test_explicit_module_required_wins`<br>`sandloom/core/tests/test_deploy_profiles.py::test_listed_modules_are_required_in_every_process` |
| G-OPS-22 | No secret file the generators write enters an sandloom new application's image. Its .dockerignore excludes env files (all but .env.example) and the key files of sandloom keys generate --out-dir (jwt\_private\_key, rpc\_private\_key, rpc\_dh\_private\_key, \*.pem) at any depth, so the split's deploy/.env.microservices (every service's private keys) stays out of the build context; its Dockerfile copies only pyproject.toml, uv.lock and the package into the build stage and nothing of the context into the runtime stage; sandloom add deploy patches an existing app's .dockerignore that lacks the exclusions; the app-split job exports the app's own build context with Docker and finds no secret file, and the smoke builds the service image from that context and finds none in it. | `sandloom/core/tests/test_image_secrets.py::test_dockerignore_rules_follow_docker`<br>`sandloom/core/tests/test_image_secrets.py::test_the_generated_context_ships_no_secret_file`<br>`sandloom/core/tests/test_image_secrets.py::test_the_generated_dockerfile_copies_only_what_it_builds`<br>`sandloom/core/tests/test_image_secrets.py::test_sandloom_add_deploy_patches_an_old_dockerignore`<br>`sandloom/core/tests/test_image_secrets.py::test_docker_agrees_the_context_has_no_secret`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_deploy_patches_a_dockerignore_that_would_ship_the_env_file`<br>`tests/e2e/test_app_split.py::test_an_sandloom_new_app_splits_into_microservices` |

### G-SAGA

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-SAGA-01 | register\_saga refuses a malformed definition deterministically at registration (duplicate steps, missing compensation, steps on the wrong side of the pivot, more than one pivot, a call that is not a command) with a fix hint. | `sandloom/core/tests/test_sagas_definition.py::test_structure_is_refused_with_a_fix`<br>`sandloom/core/tests/test_sagas_definition.py::test_contract_problems_are_refused`<br>`sandloom/core/tests/test_sagas_definition.py::test_parameter_typo_names_the_fix`<br>`sandloom/core/tests/test_sagas_definition.py::test_a_remote_provider_is_checked_against_its_snapshot` |
| G-SAGA-02 | Registering a saga adds a ContractRequirement for every called method to the module's consumes, so a missing provider fails startup. | `sandloom/core/tests/test_sagas_definition.py::test_requirements_cover_every_called_method`<br>`sandloom/core/tests/test_sagas_definition.py::test_register_saga_buffers_and_the_loader_merges_consumes` |
| G-SAGA-03 | Saga instance tables are reached across tenants only through the audited system channel under tenant RLS. On every database, with or without tenant RLS, the runtime itself scopes saga state to the bound tenant (round 3) - saga\_status and start\_saga's create-or-get read only the bound tenant's instance (no tenant bound - only a tenant-less saga), so another tenant asking for a business id gets nothing (no instance id, status or step) and its start conflicts, and a nudge moves an instance only of the tenant it names; only the system channel (sweep, operator commands) and the test harness read across tenants (saga\_store.ANY\_TENANT, a required argument of every instance read). The bound tenant is read once the unit is open (round 4) - saga\_status outside a unit opens its own, which binds SINGLE\_TENANT\_ID under TENANCY\_MODE=single, so a canonical-tenant saga is found, not reported absent. | `sandloom/core/tests/test_sagas_channel.py::test_rls_databases_go_through_the_system_channel`<br>`sandloom/core/tests/test_sagas_channel.py::test_problem_only_for_rls_without_a_system_channel`<br>`sandloom/core/tests/test_sagas_ops.py::test_saga_state_never_crosses_tenants_without_rls`<br>`sandloom/core/tests/test_sagas_ops.py::test_single_tenant_status_and_start_see_the_canonical_tenants_saga` |
| G-SAGA-04 | Every command step carries the derived operation key saga::::do\|undo; non-deterministic arguments hit the command log's conflict and park the instance. | `sandloom/core/tests/test_sagas_definition.py::test_operation_key_rule`<br>`sandloom/core/tests/test_sagas_protocol.py::test_happy_path_runs_every_step_once_with_derived_keys`<br>`sandloom/core/tests/test_sagas_recovery.py::test_nondeterministic_args_hit_the_command_log_conflict_and_park` |
| G-SAGA-05 | start\_saga runs in the caller's Unit of Work (instance row and first nudge commit with the business change), is create-or-get on saga\_id, conflicts on different input, and is refused outside a UoW. | `sandloom/core/tests/test_sagas_protocol.py::test_start_publishes_the_first_nudge_in_the_callers_transaction`<br>`sandloom/core/tests/test_sagas_ops.py::test_start_is_create_or_get_and_conflicts_on_other_input`<br>`sandloom/core/tests/test_sagas_ops.py::test_start_outside_a_unit_of_work_is_refused` |
| G-SAGA-06 | Duplicate, stale or concurrent nudges never run a step twice: the row lock and version check make them no-ops, and a locked instance is skipped, not waited for. | `sandloom/core/tests/test_sagas_protocol.py::test_duplicate_deliveries_never_run_a_step_twice`<br>`sandloom/core/tests/test_sagas_protocol.py::test_committed_transition_redelivered_is_a_no_op`<br>`sandloom/core/tests/test_sagas_protocol.py::test_a_locked_instance_is_skipped_not_waited_for`<br>`sandloom/core/tests/test_sagas_protocol.py::test_malformed_nudge_is_poison`<br>`tests/laws/test_saga_laws.py::test_saga_laws_hold_outside_the_crash_then_deadline_gap`<br>`tests/laws/test_saga_laws.py::test_steps_are_idempotent_and_status_only_moves_forward`<br>`tests/laws/test_saga_laws.py::test_a_deadline_after_a_crash_resolves_the_step_before_compensating` |
| G-SAGA-07 | A crash before the step call or after the provider committed converges without re-applying the step (same key, command-log replay). | `sandloom/core/tests/test_sagas_protocol.py::test_crash_before_the_call_rolls_back_and_the_redrive_runs_the_step`<br>`sandloom/core/tests/test_sagas_protocol.py::test_crash_after_the_provider_committed_replays_the_same_key`<br>`sandloom/core/tests/test_sagas_protocol.py::test_provider_in_flight_is_transient_and_retried`<br>`tests/laws/test_saga_laws.py::test_a_deadline_after_a_crash_resolves_the_step_before_compensating` |
| G-SAGA-08 | The timer sweep drives due backoffs and deadlines at most once per interval and purges terminal instances after retention. | `sandloom/core/tests/test_sagas_ops.py::test_sweep_nudges_due_instances_once_per_interval`<br>`sandloom/core/tests/test_sagas_ops.py::test_sweep_drives_a_deadline_even_while_backing_off`<br>`sandloom/core/tests/test_sagas_ops.py::test_sweep_purges_terminal_instances_after_retention`<br>`sandloom/core/tests/test_sagas_ops.py::test_sweep_leaves_a_fresh_nudge_alone_until_an_interval_passed` |
| G-SAGA-09 | Step outcomes are classified by an explicit closed table (408/423/425/429/5xx transient; domain 4xx refused; key conflict and misconfiguration park); an unknown status is transient. | `sandloom/core/tests/test_sagas_definition.py::test_outcome_table`<br>`sandloom/core/tests/test_sagas_definition.py::test_exceptions_classify_through_the_table`<br>`sandloom/core/tests/test_sagas_recovery.py::test_401_parks_as_misconfigured`<br>`sandloom/core/tests/test_sagas_recovery.py::test_argument_binding_error_parks_as_misconfigured` |
| G-SAGA-10 | Recovery: a refusal before the pivot compensates applied steps in reverse; after the pivot, or of a compensation, it parks with an audit row; transient outcomes retry with full-jitter backoff until the deadline, then compensate or park. | `sandloom/core/tests/test_sagas_recovery.py::test_refused_before_the_pivot_compensates_the_applied_steps`<br>`sandloom/core/tests/test_sagas_recovery.py::test_refused_first_step_compensates_nothing`<br>`sandloom/core/tests/test_sagas_recovery.py::test_refused_after_the_pivot_parks_with_an_audit_row`<br>`sandloom/core/tests/test_sagas_recovery.py::test_refused_compensation_parks`<br>`sandloom/core/tests/test_sagas_recovery.py::test_transient_before_the_pivot_retries_with_full_jitter_backoff`<br>`sandloom/core/tests/test_sagas_recovery.py::test_transient_until_the_deadline_before_the_pivot_compensates`<br>`sandloom/core/tests/test_sagas_recovery.py::test_transient_after_the_pivot_parks_at_the_deadline`<br>`sandloom/core/tests/test_sagas_recovery.py::test_transient_compensation_parks_at_the_compensation_deadline`<br>`sandloom/core/tests/test_sagas_recovery.py::test_deadline_before_the_pivot_starts_compensation`<br>`sandloom/core/tests/test_sagas_recovery.py::test_deadline_after_the_pivot_parks`<br>`sandloom/core/tests/test_sagas_recovery.py::test_compensation_deadline_parks_a_backward_instance` |
| G-SAGA-11 | A step with an ambiguous outcome is resolved (re-issued with the same key) before it is compensated; a timeout racing a completing step is serialized by the row lock; also after a crash that rolled back the attempt count: a command step's first forward call writes a durable started marker in its own transaction, and a deadline resolves a step that may have applied (attempt \> 0 or the marker) before compensating. | `sandloom/core/tests/test_sagas_recovery.py::test_ambiguous_step_is_resolved_before_it_is_compensated`<br>`sandloom/core/tests/test_sagas_recovery.py::test_resolve_that_finds_a_refusal_compensates_nothing`<br>`sandloom/core/tests/test_sagas_recovery.py::test_resolved_pivot_that_applied_never_compensates`<br>`sandloom/core/tests/test_sagas_recovery.py::test_timeout_racing_a_completing_step_is_serialized_by_the_row_lock`<br>`tests/laws/test_saga_laws.py::test_a_deadline_after_a_crash_resolves_the_step_before_compensating`<br>`tests/laws/test_saga_laws.py::test_steps_are_idempotent_and_status_only_moves_forward` |
| G-SAGA-12 | No blind re-call after the command-dedupe TTL: an operator retry of a step with an unknown outcome is refused (resolve first), and the runtime parks such a step with outcome\_unknown\_after\_ttl; a call whose transition rolled back after the provider committed (only its durable started marker survives) is recovered as a transient attempt from the marker's own time, so recovery after the TTL parks it instead of re-issuing the command. | `sandloom/core/tests/test_sagas_ops.py::test_retry_refuses_an_unknown_outcome_after_the_dedupe_ttl`<br>`sandloom/core/tests/test_sagas_ops.py::test_retry_after_the_ttl_proceeds_when_the_outcome_is_known`<br>`sandloom/core/tests/test_sagas_recovery.py::test_ttl_guard_parks_an_ambiguous_step_whose_record_may_be_gone`<br>`sandloom/core/tests/test_sagas_cli.py::test_post_ttl_retry_exits_1_with_the_resolve_hint`<br>`sandloom/core/tests/test_sagas_recovery.py::test_a_crash_after_the_provider_committed_is_never_reissued_after_the_ttl` |
| G-SAGA-13 | Production refuses a saga whose retry and compensation horizon exceeds the command-dedupe TTL unless the risk is accepted. | `sandloom/core/tests/test_sagas_definition.py::test_saga_horizon_fits_the_dedupe_ttl`<br>`sandloom/core/tests/test_sagas_definition.py::test_saga_horizon_refuses_in_prod_unless_accepted` |
| G-SAGA-14 | An instance of an unregistered definition version parks with unknown\_definition instead of crashing the consumer. | `sandloom/core/tests/test_sagas_protocol.py::test_unknown_definition_parks_instead_of_crashing`<br>`sandloom/core/tests/test_sagas_cli.py::test_doctor_warns_about_parked_and_unknown_versions` |
| G-SAGA-15 | Operator mutations (retry, park, resolve) are dry runs unless --apply and are audited. | `sandloom/core/tests/test_sagas_cli.py::test_mutations_are_dry_runs_without_apply`<br>`sandloom/core/tests/test_sagas_ops.py::test_park_then_retry_resumes_with_audit_rows`<br>`sandloom/core/tests/test_sagas_ops.py::test_resolve_requires_a_parked_instance`<br>`sandloom/core/tests/test_sagas_ops.py::test_resolve_skipped_compensation_moves_backward` |

### G-SDK

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-SDK-01 | Importing a public sandloom.sdk module, sandloom.sdk.testing or a bundled module (identity, tenancy, billing) is side-effect free: with python -I, an empty environment and no .env it reads no environment variable, constructs no Settings, opens no socket or non-package file, installs no logging, signal or atexit handler, starts no thread and builds no engine or client; help(sandloom.sdk.events) works. Settings are lazy and every entry point forces them (enforce\_configuration / load\_settings), so an invalid environment still fails at startup. | `tests/architecture/test_sdk_import_isolation.py::test_sdk_import_is_side_effect_free`<br>`tests/architecture/test_sdk_import_isolation.py::test_the_audit_sees_an_entry_point_forcing_settings`<br>`sandloom/core/tests/test_lazy_settings.py::test_import_does_not_validate_and_first_use_does`<br>`sandloom/core/tests/test_lazy_settings.py::test_an_invalid_environment_still_fails_at_the_entry_point`<br>`sandloom/core/tests/test_lazy_settings.py::test_loaded_settings_are_a_plain_settings_instance` |
| G-SDK-02 | sandloom.sdk imports only the public kernel surface (no kernel internal, no private submodule of a public kernel package); the kernel (sandloom.core, sandloom.shared) never imports sandloom.sdk; L1 (sandloom.sdk.\_decl) imports no kernel module; and sandloom.sdk is layered public modules \> \_decl \> \_prim. | `tests/architecture/test_kernel_import_contracts.py::test_kernel_import_contracts_are_kept`<br>import contract `sdk-kernel-surface`<br>import contract `kernel-never-imports-sdk`<br>import contract `sdk-decl-l1`<br>import contract `sdk-layers` |
| G-SDK-03 | L1 registration equals L2: for a fixture module per L1 feature (router, producer events, views and consumers, a contract provider, a saga with a typed step, a projection with full-state, tombstone and delta groups) and for every example module, the descriptor and the recorded registration calls built by sandloom.sdk define\_module equal those of a hand-written L2 register; the module\_router routes carry exactly the per-route dependencies of the L2 form. | `tests/sdk/test_sdk_l1_registration.py::test_l1_equals_l2_registration`<br>`tests/sdk/test_sdk_l1_registration.py::test_module_router_routes_equal_the_l2_router`<br>`tests/sdk/test_sdk_l1_registration.py::test_example_modules_register_identically_through_l1` |
| G-SDK-04 | Generated code (sandloom new, every sandloom add generator) and every example file that imports sandloom.sdk use only the 29 beginner names; an advanced name needs `# sandloom: advanced <reason>` on its import (SANDLOOM-T009). | `tests/sdk/test_sdk_generators.py::test_the_beginner_tier_is_the_29_names`<br>`tests/sdk/test_sdk_generators.py::test_generated_code_uses_only_the_beginner_tier`<br>`tests/sdk/test_sdk_generators.py::test_examples_importing_the_sdk_stay_in_the_beginner_tier`<br>`tests/sdk/test_sdk_generators.py::test_the_pin_reports_an_unmarked_advanced_name` |
| G-SDK-05 | Every name sandloom.sdk.\_decl (L1) imports from sandloom.sdk.\_prim is exported by a public sandloom.sdk module, and every \_prim name is public: each L1 dependency is an advertised L2 primitive; the H1 names (public, at\_commit, db, open\_uow, sensitive\_effect, MoneyLedger, Account, authorizes, ...) are the kernel objects themselves, no wrapper. | `tests/architecture/test_public_api_sdk.py::test_l1_uses_only_public_l2`<br>`tests/architecture/test_public_api_sdk.py::test_h1_names_are_the_kernels`<br>`tests/architecture/test_public_api_sdk.py::test_db_and_open_uow_are_the_kernel_units`<br>`tests/architecture/test_public_api_sdk.py::test_public_is_the_kernel_marker` |

### G-DECL

| Id | Statement | Enforced by |
| --- | --- | --- |
| G-DECL-01 | An Event class is a producer when its channel prefix is the defining module (or stated), else a view; channels are versioned, fields are from the closed set of wire shapes (round 10: an event payload is plain data - scalars (str, int, float, bool, None, UUID, datetime, date, time, timedelta, Decimal, Enum, Literal, Any as an opaque JSON value), X \| None and (round 11) unions of Literal / Enum members none of which reads another member's published value, list / tuple / set and frozenset of scalars / dict\[str, T\], pydantic BaseModel DTOs, and unions of DTOs only when discriminated by a Literal tag field; every other shape at any depth is refused at class definition with SANDLOOM-B015 naming the field path and the fix: (round 11) a union of scalars whose members read the same value (int \| str, UUID \| str, float \| int, datetime \| str, Literal\[1\] \| Literal\[True\]) in any union\_mode, strict or not, an empty wire name (alias, serialization\_alias or an alias\_generator returning ""), two fields with one wire name or a field named like another field's wire name in a model that reads by name, a model\_config option outside the whitelist (ser\_json\_timedelta / temporal / inf\_nan / bytes, val\_json\_bytes, val\_temporal\_unit, coerce\_numbers\_to\_str, regex\_engine, use\_enum\_values, polymorphic\_serialization, json\_encoders, json\_schema\_mode\_override, allow\_inf\_nan=True, any unlisted key), a float that may be NaN or infinite (the Event base sets allow\_inf\_nan=False; a DTO states it), SerializeAsAny and any other node serializer, coerce\_numbers\_to\_str on a field - so every class that defines round-trips: a property test over random classes from the whole set and from the refused shapes and options holds every one that defines to to\_wire, standard JSON (no NaN), from\_wire returning the same value and type at every depth, agreeing with the strict class's model\_validate\_json (fields set included); a rebuilt class (model\_rebuild) is checked again and read by a reader of its new schema; from\_wire refuses a DTO instance (it reads JSON data) -, an undiscriminated union holding a DTO or a container in any union\_mode, a callable discriminator, a DTO overriding **init** or **new**, a mode=before / wrap validator over a DTO or a plain validator, a custom serializer or computed field, a wire name that does not round-trip - serialization\_alias or validation\_alias alone, AliasChoices, AliasPath, an alias the model does not read -, an excluded field without a default, dataclasses, TypedDicts, NamedTuples, RootModels, unparametrised generics, non-JSON scalars, non-str dict keys, sets of non-scalars, and a forward reference left unresolved), one producer per channel and module, a stated producer outside its module is refused (SANDLOOM-B015); a view never publishes (SANDLOOM-C025 at run time). A producer registers its wire schema (E.wire\_schema(): the serialization schema, so a defaulted field is required, with no additionalProperties: false, so a reader accepts a newer producer's added optional field; construction stays strict), and it is snapshotted (module.snapshot.json) and exported to the shared DTO package; a change on the same .vN other than an added optional field fails before anything is written (SANDLOOM-T005), in dev and CI, at every depth: $ref / $defs are resolved, so a referenced enum's values or a nested DTO's field types, removals and newly required fields fail like top-level ones, while an added defaulted (optional) field passes at any depth - a default of any kind, literal or factory (round 7: Field(default\_factory=list / dict / set / a model) carries no JSON default, so the wire schema marks every defaulted property "x-sandloom-default": true and the rule and the live schema registry read that mark; a new reader fills the field in an old payload, an old reader ignores it in a new one, while the same field added without a default still fails as newly required); a field that loses its default fails - literal or factory, top level or nested - although the serialization schema's required list is unchanged, since a producer from before the field omits it (round 8: the rule and the live schema registry, which also compares every shared $defs entry, read the lost mark), while adding a default to a required field passes; the mark is framework-owned: a json\_schema\_extra that sets "x-sandloom-default" (a field, a nested model, a callable) is refused at class definition (SANDLOOM-B015), so it always means "has a default"; readers ignore unknown keys at every depth (round 9: from\_wire validates once with a lenient twin of the class's own validation schema, in which every extra="forbid" DTO - nested, in discriminated unions, lists, dicts, tuples and recursive definitions - ignores extra keys while extra="allow" ones keep theirs; aliases, discriminators and validators are the class's own and run once, the result and every nested value are instances of their real classes, equal to constructed ones, and a real error is raised as is; a FailFast list of any length reads; round 10: the reader is built when the class is defined and compiling it modifies no class - its model nodes name factory classes whose **new** allocates the real class, which pydantic-core fills and post-inits - so concurrent first decodes never cache a strict reader, nothing in the reader is strict (a strict=True DTO reads its published strings), and a self-check verifies that pydantic-core contract on a canary before the first reader is stored, failing loudly with the pydantic versions if an upgrade breaks it), so an optional field added inside a strict nested DTO is safe for its old reader, the live schema registry compares enum members as JSON values (a list or object member, mixed types), and the wire schema carries no additionalProperties: false at any depth; a recursive DTO (self- or mutually recursive) is compared once per ($ref old, $ref new) pair on a descent, so it never recurses forever and a change inside the cycle still fails; a validation constraint that narrows the accepted values fails (a bound raised / lowered or added, inclusive to exclusive, minLength / maxLength / minItems / maxItems / minProperties / maxProperties tightened or added, multipleOf added or no longer dividing the old one, pattern added or changed, uniqueItems or additionalProperties: false added; format and const changes fail as type changes), and so does one that loosens them (round 6: producers and consumers of one .vN deploy independently, so the rule holds both directions - a new reader accepts every payload an old producer may send AND an old reader accepts every payload a new producer may send: a loosened or removed bound, size, multipleOf, pattern, uniqueItems, additionalProperties: false, dependentRequired, contains or then / else / dependentSchemas constraint fails, and a model's field is never removed, open to extra keys or not - removing a required field from an extra="allow" model fails since the new producer omits it and the old reader requires it, and an optional field added to a model whose old schema forbids extra keys fails); every subschema applicator is traversed with the same rule (round 3) - a dictionary's key constraints (propertyNames) and its regex-keyed values (patternProperties: a value change under a pattern fails, an added, changed or removed key pattern fails), tuple positions (prefixItems), contains (added or narrowed) / minContains / maxContains, then / else, dependentSchemas and dependentRequired (added or narrowed), unevaluatedItems / unevaluatedProperties (narrowed, or false added) and items: false added, while any change of a not or if subschema fails; required is compared in every subschema with or without properties (round 4) - a dictionary's or a dependentSchemas / then / allOf subschema's added required name fails - an added property is compared with the schema that governed that key (a dictionary's additionalProperties or matching patternProperties, {} below an overlaying applicator), only a closed DTO (no additionalProperties, or false) may gain an optional field no old producer sent - a model open to extra keys (extra="allow": additionalProperties true or a schema) is an open object, so a field it newly declares is compared with what its old additionalProperties allowed (true: only a field accepting anything passes; round 5) -, a keyword beside a $ref is compared on its own, and a removed key pattern's keys are compared with the new additionalProperties; a differential test holds the rule to the framework validator - for every pair of a corpus of mutated pydantic wire schemas (every narrowing and loosening keyword at every position and under every applicator, plus hypothesis-composed chains; extra="allow" models, field removals, and additionalProperties true / a schema added to a DTO, included) the rule accepts, no payload sampled from the old schema fails the new one and no payload sampled from the new schema fails the old one (round 6; the sampler sends undeclared keys for a dictionary or an open model; round 8: the sampler omits defaulted fields as a producer from before them would, readers fill them, and the corpus removes defaults); the differential also runs on pairs of real Event classes with the REAL readers (from\_wire of the old and new class: additions in strict, lax and open nested DTOs, lists, unions and dicts - round 10: every allowed shape - each scalar and container of scalars beside an added field, a DTO alone, optional, in lists, variadic and fixed tuples, dicts, deep containers and FailFast lists, discriminated unions alone, optional, in lists and dicts, strict, lax, open, aliased, recursive, validated and two-level nested DTOs -, and Decimal / UUID / Enum retypings, which fail (a Decimal is format: decimal in the wire schema, so its retyping to str is a type change the old Decimal reader would reject), default removals, added required and removed fields), and every such class's wire schema accepts exactly what its real reader accepts, an undeclared key at every depth included; the static view check (SANDLOOM-C025) sees fields inherited from a same-file abstract base. A field named like the Event API (channel, meta, publish, wire\_schema) is refused at class definition, abstract bases included, with SANDLOOM-B015 naming the name and the fix (another name, or Field(alias=...)). | `tests/sdk/test_sdk_events.py::test_channel_must_be_versioned`<br>`tests/sdk/test_sdk_events.py::test_reserved_and_non_wire_fields_are_refused`<br>`tests/sdk/test_sdk_events.py::test_a_reserved_field_name_fails_at_definition`<br>`tests/sdk/test_sdk_events.py::test_role_is_derived_from_the_channel_prefix`<br>`tests/sdk/test_sdk_events.py::test_a_view_cannot_publish`<br>`tests/sdk/test_sdk_discovery.py::test_producers_are_registered_and_views_are_not`<br>`tests/sdk/test_sdk_discovery.py::test_two_producers_of_one_channel_are_refused`<br>`tests/sdk/test_sdk_generated.py::test_only_an_optional_field_may_be_added_on_the_same_version`<br>`tests/sdk/test_sdk_generated.py::test_a_referenced_enum_or_nested_type_change_is_incompatible`<br>`tests/sdk/test_sdk_generated.py::test_an_added_optional_field_passes_at_every_depth`<br>`tests/sdk/test_sdk_generated.py::test_a_recursive_dto_compares_without_recursing_forever`<br>`tests/sdk/test_sdk_generated.py::test_a_tightened_constraint_is_incompatible`<br>`tests/sdk/test_sdk_generated.py::test_a_loosened_constraint_is_incompatible_too`<br>`tests/sdk/test_sdk_generated.py::test_an_unchanged_constraint_is_compatible`<br>`tests/sdk/test_sdk_generated.py::test_a_changed_format_or_const_is_incompatible`<br>`tests/sdk/test_sdk_generated.py::test_unique_items_and_forbidden_extras_narrow_a_nested_dto`<br>`tests/sdk/test_sdk_generated.py::test_a_tightened_dictionary_key_constraint_is_incompatible`<br>`tests/sdk/test_sdk_generated.py::test_a_regex_keyed_dictionary_is_compared_per_pattern`<br>`tests/sdk/test_sdk_generated.py::test_every_applicator_follows_the_narrowing_rule`<br>`tests/sdk/test_sdk_generated.py::test_a_loosened_applicator_is_incompatible_too`<br>`tests/sdk/test_sdk_generated.py::test_an_unchanged_applicator_is_compatible`<br>`tests/sdk/test_sdk_generated.py::test_a_constraint_on_a_node_without_properties_is_compared`<br>`tests/sdk/test_sdk_generated.py::test_a_constraint_beside_a_ref_and_a_removed_key_pattern_are_compared`<br>`tests/sdk/test_sdk_schema_differential.py::test_a_change_the_rule_accepts_rejects_no_payload_either_way`<br>`tests/sdk/test_sdk_schema_differential.py::test_composed_changes_the_rule_accepts_reject_no_payload_either_way`<br>`tests/sdk/test_sdk_schema_differential.py::test_a_field_removed_from_an_open_model_breaks_its_old_readers`<br>`tests/sdk/test_sdk_schema_differential.py::test_a_loosened_constraint_breaks_its_old_readers`<br>`tests/sdk/test_sdk_schema_differential.py::test_the_corpus_covers_every_applicator_position`<br>`tests/sdk/test_sdk_schema_differential.py::test_a_field_declared_on_an_open_model_must_accept_its_old_extras`<br>`tests/sdk/test_sdk_schema_differential.py::test_a_field_added_with_a_default_factory_is_compatible_both_ways`<br>`tests/sdk/test_sdk_schema_differential.py::test_a_field_added_without_a_default_is_still_required_now`<br>`tests/integration/test_review_hardening.py::test_the_registry_accepts_a_field_added_with_a_default_factory`<br>`tests/sdk/test_sdk_schema_differential.py::test_removing_a_default_breaks_a_producer_from_before_the_field`<br>`tests/sdk/test_sdk_schema_differential.py::test_the_registry_refuses_a_removed_default`<br>`tests/sdk/test_sdk_schema_differential.py::test_an_old_strict_nested_reader_ignores_a_newer_producers_field`<br>`tests/sdk/test_sdk_schema_differential.py::test_a_user_cannot_forge_the_default_mark`<br>`tests/sdk/test_sdk_schema_differential.py::test_the_gate_agrees_with_the_real_readers`<br>`tests/sdk/test_sdk_schema_differential.py::test_the_wire_schema_is_what_the_real_reader_accepts`<br>`tests/sdk/test_sdk_event_reader.py::test_round10_concurrent_first_decodes_cache_a_lenient_reader`<br>`tests/sdk/test_sdk_event_reader.py::test_round10_an_undiscriminated_union_with_a_dto_is_refused`<br>`tests/sdk/test_sdk_event_reader.py::test_round10_a_custom_init_or_new_is_refused_at_any_depth`<br>`tests/sdk/test_sdk_event_reader.py::test_round10_a_wire_name_that_does_not_round_trip_is_refused`<br>`tests/sdk/test_sdk_event_reader.py::test_a_discriminated_or_aliased_payload_is_read_by_its_own_paths`<br>`tests/sdk/test_sdk_event_reader.py::test_a_fail_fast_list_of_any_length_reads_every_newer_item`<br>`tests/sdk/test_sdk_event_reader.py::test_a_strict_dto_reads_the_wire_form_of_every_scalar`<br>`tests/sdk/test_sdk_event_reader.py::test_a_forward_reference_is_refused_at_definition`<br>`tests/sdk/test_sdk_event_reader.py::test_the_reader_self_check_passes_on_the_pinned_pydantic`<br>`tests/sdk/test_sdk_event_reader.py::test_dicts_tuples_and_recursive_models_ignore_unknown_keys`<br>`tests/sdk/test_sdk_event_reader.py::test_a_broken_reader_contract_fails_loudly_naming_pydantic`<br>`tests/sdk/test_sdk_event_reader.py::test_an_open_model_keeps_its_extras_and_its_strict_children_ignore`<br>`tests/sdk/test_sdk_event_reader.py::test_validators_run_once_and_real_errors_are_raised`<br>`tests/sdk/test_sdk_event_reader.py::test_the_reader_returns_the_real_classes_and_construction_stays_strict`<br>`tests/sdk/test_sdk_schema_differential.py::test_every_refused_shape_raises_at_definition`<br>`tests/sdk/test_sdk_schema_differential.py::test_every_bundled_event_is_in_the_closed_set`<br>`tests/integration/test_review_hardening.py::test_enum_values_compare_as_json_values`<br>`tests/sdk/test_sdk_generated.py::test_an_incompatible_change_fails_before_anything_is_written`<br>`tests/sdk/test_sdk_generated.py::test_plan_generates_the_l1_module_files`<br>`tests/sdk/test_sdk_events.py::test_the_wire_schema_requires_defaults_and_accepts_added_fields`<br>`tests/architecture/test_api10_checks.py::test_c025_sees_fields_inherited_from_an_abstract_base`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_a_union_of_overlapping_scalars_is_refused`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_a_disjoint_literal_union_round_trips`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_an_empty_wire_name_is_refused`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_colliding_wire_names_are_refused`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_a_config_that_changes_the_wire_form_is_refused`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_field_options_that_change_the_wire_form_are_refused`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_the_allowed_configuration_keys_round_trip`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_from_wire_reads_json_data_not_instances`<br>`tests/sdk/test_sdk_event_reader.py::test_round11_a_rebuilt_class_gets_a_fresh_reader`<br>`tests/sdk/test_sdk_event_roundtrip.py::test_every_event_that_defines_round_trips`<br>`tests/sdk/test_sdk_event_roundtrip.py::test_every_allowed_event_defines_and_round_trips`<br>`tests/sdk/test_sdk_event_roundtrip.py::test_the_property_holds_on_the_round11_repros` |
| G-DECL-02 | @on(E) registers through register\_event\_consumer with every option (name, durable, required, dead\_letter, start\_from, group, database) and binds the tenant from a `tenant_id: UUID` field (or a named UUID field, or none); a handler whose parameter is not E, a non-async handler, a duplicate name or a bad tenant field is refused (SANDLOOM-B016); deliveries decode the payload into E (unknown fields ignored) with EventMeta attached and never part of the payload. | `tests/sdk/test_sdk_events.py::test_on_derives_the_tenant_binding_and_keeps_every_option`<br>`tests/sdk/test_sdk_events.py::test_on_refuses_bad_handlers`<br>`tests/sdk/test_sdk_events.py::test_callback_decodes_the_payload_and_attaches_meta`<br>`tests/sdk/test_sdk_discovery.py::test_duplicate_consumer_names_are_refused` |
| G-DECL-03 | define\_module derives consumes from module-level Contract(...) references: name and required exact; methods only from direct `<name>.<method>(` calls on the name or a direct alias, any other use giving () (every method); merged with explicit consumes never weaker (required ORed, methods unioned or ()); a saga's contract requirements merge into consumes by name the same way. | `tests/sdk/test_sdk_contracts.py::test_methods_are_derived_from_direct_calls_only`<br>`tests/sdk/test_sdk_discovery.py::test_consumes_are_derived_and_merged_never_weaker`<br>`sandloom/core/tests/test_saga_requirement_merge.py::test_an_undeclared_contract_is_added_once`<br>`sandloom/core/tests/test_saga_requirement_merge.py::test_a_declared_all_methods_entry_absorbs_the_sagas`<br>`sandloom/core/tests/test_saga_requirement_merge.py::test_methods_union_required_or_and_the_declared_reason_kept` |
| G-DECL-04 | Contract(REF, optional=True).client() is None only when the provider is neither local nor bound in the deployment (ServiceNotFound / ServiceUnavailableInDeployment); a deployed provider is returned even when it will fail (503 on the call). | `tests/sdk/test_sdk_contracts.py::test_optional_is_none_only_when_not_deployed`<br>`tests/sdk/test_sdk_contracts.py::test_optional_returns_a_deployed_provider_even_if_it_will_fail` |
| G-DECL-05 | sandloom check SANDLOOM-C021 refuses a function-local import in a business module (typing-only imports and a marked optional dependency excepted). SANDLOOM-C021 is un-planned and active in sandloom check: the bundled modules (identity, tenancy, billing) and the examples carry no function-local import (qrcode keeps a deferred import marked \# sandloom: optional-import), so the framework tree and the examples report none. | `tests/architecture/test_api10_checks.py::test_c021_function_local_imports`<br>`tests/architecture/test_api10_checks.py::test_every_api10_rule_has_its_appendix_a_code`<br>`tests/architecture/test_api10_checks.py::test_the_trees_pass_the_10_checks_once_unplanned` |
| G-DECL-06 | A typed event field with a wire name (Field(alias=...)) round-trips: publish() emits the alias, the registered and exported schema names it, and every reader (an @on consumer of the producer class or of a view, a projection, published(), deliver()) decodes it back from the wire key; @on and a projection bind the tenant from the field's wire name. | `tests/sdk/test_sdk_events.py::test_an_aliased_field_survives_publish_deliver_and_handler`<br>`tests/sdk/test_sdk_events.py::test_on_binds_the_tenant_by_its_wire_name`<br>`tests/sdk/test_sdk_testing_kit.py::test_an_aliased_field_survives_the_real_consumer_runtime` |
| G-DECL-07 | SANDLOOM-C012. A contract DTO (a model / Enum a @contract signature names, nested ones included, except classes of sandloom.shared and generated copies) is held to the closed set of event wire shapes (G-DECL-01) within the contract wire types, plus what its generated copy can carry: no parametrised generic, a default the copy can write (JSON scalars, Enum members, UUID / Decimal / date / datetime / time / timedelta, containers of them) and default\_factory only list / dict / set / frozenset / tuple, Field constraints only (no custom annotated constraint, no StringConstraints transformation, no callable discriminator, no exclude\_if), no Flag enum; anything else is refused when @contract decorates the provider (ContractDtoError) naming the method, the field path and the fix. The copy carries fields, types and constraints, defaults, aliases (an alias\_generator resolved per field), Literal / Enum values, nested and discriminated models and the allowed model\_config options - never methods or validators, which stay the provider's; over the whole set (a property test over random payloads) the copy accepts and refuses exactly what the provider's class does, in python and JSON mode, and reads accepted payloads to the same data. Contract wire dumps use each field's wire name (by\_alias), so an aliased DTO round-trips. | `tests/sdk/test_sdk_contract_dtos.py::test_a_dto_the_copy_cannot_carry_is_refused_with_its_path`<br>`tests/sdk/test_sdk_contract_dtos.py::test_the_copy_accepts_exactly_what_the_provider_accepts`<br>`tests/sdk/test_sdk_contract_dtos.py::test_every_copied_class_is_marked_and_carries_the_wire_shape`<br>`tests/sdk/test_sdk_contract_dtos.py::test_a_validator_stays_the_providers`<br>`tests/sdk/test_sdk_contract_dtos.py::test_a_caller_receives_the_copy_equal_to_the_providers_value` |

## Capabilities

### C-TENANT

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-TENANT-01 | A model opts into fail-closed tenant scoping by inheriting sandloom.core.orm.TenantScoped, which declares the tenant\_id column and registers it with the ORM guard. | `sandloom/core/tests/test_tenant_isolation.py::test_automatic_filtering_to_bound_tenant`<br>`sandloom/core/tests/test_tenant_guard_cache.py::test_new_tenant_model_invalidates_caches`<br>`sandloom/core/tests/test_ledger_runtime_pins.py::test_tenant_scoped_column_is_indexed_and_required` |
| C-TENANT-02 | Endpoints bind the request tenant with the TenantScope() dependency (configurable header and claim keys, required or optional), and workers/scripts bind one with bind\_tenant(id). | `sandloom/core/tests/test_tenant_isolation.py::test_header_can_select_tenant_from_token_allowlist`<br>`sandloom/core/tests/test_tenant_isolation.py::test_automatic_filtering_to_bound_tenant`<br>**gap (S)**: TenantScope(required=False) yielding None and leaving the context unbound, and custom header/claim\_keys, are untested. |
| C-TENANT-03 | Deliberate cross-tenant admin/system access on the ORM path is available with an explicit `with unscoped():` block (including bulk-by-PK updates), and logs a one-time warning when used with no tenant on an RLS database. | `sandloom/core/tests/test_tenant_isolation.py::test_unscoped_sees_all_tenants`<br>`sandloom/core/tests/test_tenant_guard_gaps.py::test_bulk_update_by_primary_key_allowed_when_unscoped`<br>`sandloom/core/tests/test_tenant_rls_bypass.py::test_unscoped_without_tenant_warns_once` |
| C-TENANT-04 | Single-tenant deployments run with TENANCY\_MODE=single plus SINGLE\_TENANT\_ID, binding that canonical tenant everywhere as a strict special case of the same isolation model. | `sandloom/core/tests/test_tenant_isolation.py::test_single_tenant_mode_binds_canonical_tenant`<br>`sandloom/core/tests/test_open_uow.py::test_single_tenant_scope_bound_only_inside` |
| C-TENANT-05 | PostgreSQL Row-Level Security is opt-in per database instance with INFRA database option tenant\_rls=true (installing the per-transaction sandloom.tenant\_id hook and advertising the tenant\_rls feature) plus enable\_tenant\_rls/disable\_tenant\_rls in module migrations. | `sandloom/core/tests/test_tenant_rls.py::test_driver_option_installs_hook`<br>`sandloom/core/tests/test_database_tenant_rls_feature.py::test_feature_set_follows_option`<br>`sandloom/core/tests/test_database_tenant_rls_feature.py::test_startup_preflight_checks_instance_options`<br>`examples/projecthub/projecthub/modules/taskboard/tests/test_rls_migration.py::test_taskboard_installs_forced_tenant_rls` |
| C-TENANT-06 | Kernel/ops code can run audited cross-tenant work through sandloom.core.drivers.system\_db.system\_session(actor=, reason=, database=, tenant\_id=) over a separate sandloom\_system login (DATABASE\_SYSTEM\_URL / system\_url). | `sandloom/core/tests/test_tenant_rls_bypass.py::test_system_channel_reaches_all_tenants_and_is_audited`<br>`sandloom/core/tests/test_tenant_rls_bypass.py::test_system_session_tenant_scoped_and_rollback` |
| C-TENANT-07 | PRODUCTION\_PROFILE=multi\_tenant\_strict is available to turn the multi-tenant trust contract (ORM guard + RLS on every database, fail-closed revocation, separate security Redis, split checks) into enforced boot and startup checks. | `sandloom/core/tests/test_production_profile.py::test_compliant_strict_profile_starts_and_forces_fail_closed` |
| C-TENANT-08 | Security-critical cache state can be routed to its own Redis with SECURITY\_CACHE\_INSTANCE, and idempotency records optionally to another with IDEMPOTENCY\_CACHE\_INSTANCE (default empty = follows the security cache). | `sandloom/core/tests/test_idempotency_cache_instance.py::test_unset_follows_the_security_cache`<br>`sandloom/core/tests/test_idempotency_cache_instance.py::test_default_keeps_guard_records_in_the_security_cache` |
| C-TENANT-09 | A deliberately global table with a tenant\_id column can be exempted from the tenant-scoped-models rule via tenant\_scope\_exempt in architecture\_rules.yml with a written reason. | `tests/architecture/test_tenant_scoped_models_rule.py::test_rule_runs_in_sandloom_check_and_honours_exemptions`<br>rule `tenant-scoped-models` |
| C-TENANT-10 | The TenantScoped mixin declares no plain tenant\_id index on a table whose primary key, unique constraint or unique index leads with tenant\_id (that key serves the lookups), and keeps it otherwise. | `sandloom/core/tests/test_model_indexes_migrated.py::test_tenant_index_is_dropped_when_a_unique_key_leads_with_tenant_id` |

### C-EVENT

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-EVENT-01 | publish\_event\_versions dual-publishes one event on several channel versions (.v1 + .v2) through the outbox, re-exported from sandloom.core.di, so a channel can be rolled without starving live readers. | `sandloom/core/tests/test_streams_split_identity.py::test_publish_event_versions_is_reexported_for_modules`<br>`sandloom/core/tests/test_streams_split_identity.py::test_dual_publish_does_not_warn_about_starvation`<br>`sandloom/core/tests/test_uow_outbox.py::test_publish_event_versions_outside_uow_raises_unless_best_effort`<br>**gap (S)**: No test proves that publish\_event\_versions inside a UoW writes all versions as outbox rows atomically (rolled back together). |
| C-EVENT-02 | Modules react to other modules' events through core.register\_event\_consumer(channel, handler, name=...) with options group=, start\_from=, bind\_tenant\_from=, dead\_letter=, durable=, required=, all forwarded by the di helper through one signature. | `sandloom/core/tests/test_event_consumers.py::test_module_registration_applies_event_consumer`<br>`sandloom/core/tests/test_event_consumers.py::test_register_event_consumer_has_one_signature`<br>`sandloom/core/tests/test_event_consumers.py::test_di_register_event_consumer_forwards_every_option` |
| C-EVENT-03 | sandloom streams inventory reports live consumers per channel version (flagging starved versions) and per-group lag/pending, used to decide when a .v1 reader is gone. | `sandloom/core/tests/test_streams_split_identity.py::test_inventory_lists_live_consumers_and_flags_starved_versions`<br>`sandloom/core/tests/test_streams_split_identity.py::test_group_lag_and_pending_are_reported`<br>`sandloom/core/tests/test_streams_split_identity.py::test_streams_cli_operations` |
| C-EVENT-04 | tenancy.member\_added / member\_removed / member\_role\_changed .v2 (tenant\_id, required per-membership version) are dual-published with v1, and billing.subscription.\*.v1 carry the catalog plan in data.plan. | `sandloom_tenancy/tests/test_registration_surface.py::test_registration_surface_is_pinned`<br>`sandloom_billing/tests/test_entitlement_intentless.py::test_subscription_events_carry_whether_the_subscription_grants` |

### C-EFFECT

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-EFFECT-01 | The framework Celery worker (celery -A sandloom.core.worker) boots the composition root, takes its app from the taskqueue capability and imports .tasks plus setup\_periodic\_tasks of every loaded module. | `sandloom/core/tests/test_worker.py::test_worker_registers_loaded_module_tasks_and_beat_schedule`<br>`sandloom/core/tests/test_worker.py::test_worker_without_identity_has_no_identity_tasks`<br>`sandloom/core/tests/test_worker.py::test_worker_honours_taskqueue_instance_env`<br>`sandloom/core/tests/test_worker.py::test_non_celery_taskqueue_is_rejected` |
| C-EFFECT-02 | The sandloom.core.testing seams run\_on\_commit / bind\_test\_uow / recording\_uow let tests own the transaction while still running before\_commit checks then on\_commit callbacks, and capture required audits. | `sandloom/core/tests/test_before_commit.py::test_testing_run_on_commit_runs_the_before_commit_checks_first`<br>`sandloom/core/tests/test_testing_harness.py::test_recording_uow_captures_required_audits` |

### C-AUTH

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-AUTH-01 | SSO login/link over OAuth2 with one-time, TTL-bound, browser-bound CSRF state tokens (reuse and expiry refused; link requires an authenticated link state). | `sandloom_identity/tests/api/test_sso.py::TestSSOSecurityReal::test_state_reuse_protection_real`<br>`sandloom_identity/tests/api/test_sso.py::TestSSOSecurityReal::test_sso_state_expiration_real`<br>`sandloom_identity/tests/api/test_sso_hardening.py::test_login_csrf_victim_browser_cannot_complete_attacker_flow`<br>`sandloom_identity/tests/api/test_sso_hardening.py::test_link_flow_requires_authenticated_link_state` |
| C-AUTH-02 | TOTP 2FA with single-use codes (replay refused) and single-use backup codes, secrets encrypted at rest. | `sandloom_identity/tests/services/test_twofa_service.py::TestTOTPReplay::test_same_code_cannot_be_used_twice`<br>`sandloom_identity/tests/services/test_twofa_service.py::TestBackupCodes::test_backup_code_single_use`<br>`sandloom_identity/tests/services/test_twofa_service.py::TestSecretCrypto::test_undecryptable_secret_fails_loudly` |
| C-AUTH-03 | Asymmetric JWT split: `sandloom keys generate` produces a usable key pair (stdout or SECRETS\_DIR files), identity declares itself the token issuer (issues\_tokens=True), and split services verify with public keys only. | `sandloom/core/tests/test_asymmetric_jwt.py::test_keys_generate_prints_a_usable_pair`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_keys_generate_writes_secrets_dir_files`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_identity_declares_itself_the_token_issuer`<br>`tests/split_conformance/test_split_conformance.py::test_identity_issues_asymmetric_tokens` |
| C-AUTH-04 | Emergency stop: block\_user / unblock\_user from sandloom.core.di plus identity's platform-admin block / unblock / status / revoke-sessions endpoints (Idempotency-Key, audited, operator cannot block themself); the `sandloom security` incident CLI is area F. | `sandloom_identity/tests/api/test_user_safety.py::test_block_status_unblock_round_trip`<br>`sandloom_identity/tests/api/test_user_safety.py::test_block_requires_idempotency_key_and_replays_once`<br>`sandloom_identity/tests/api/test_user_safety.py::test_operator_cannot_block_or_end_own_sessions`<br>`sandloom_identity/tests/api/test_user_safety.py::test_revoke_sessions_revokes_refresh_tokens_and_audits` |
| C-AUTH-05 | Refresh-token rotation with reuse detection: each refresh rotates within the family, reuse of a rotated token revokes the whole family, a benign replay within the grace window is 409, concurrent refreshes yield one winner. | `sandloom_identity/tests/services/test_refresh_rotation.py::test_refresh_rotates_within_family`<br>`sandloom_identity/tests/services/test_refresh_rotation.py::test_reusing_rotated_token_revokes_whole_family`<br>`sandloom_identity/tests/services/test_refresh_rotation.py::test_replay_within_grace_window_is_409_without_revocation`<br>`sandloom_identity/tests/services/test_refresh_rotation.py::test_concurrent_refresh_only_one_wins` |
| C-AUTH-06 | Composable authz algebra (require / enforce / And / Or / Not over Perm, Role, Owner, Subscribed, SubscriptionActive, Credits, Entitled; the billing atoms take a required of=) as the single enforcement path. | `tests/integration/test_authz_algebra.py::test_perm_and_role_membership`<br>`tests/integration/test_authz_algebra.py::test_and_or_not`<br>`tests/integration/test_authz_algebra.py::test_owner_loads_resource_once`<br>`tests/integration/test_access_subsystem.py::test_enforce_allows_and_returns_decision` |
| C-AUTH-07 | subject\_commit\_check(request), at\_commit() (an explicit commit obligation on a non-write route; replaces revocation\_at\_commit()) and before\_commit(callback) as reusable kernel hooks; the re-check runs before the COMMIT of every committing unit of the request and is automatic on writes and writing GETs. | `sandloom/core/tests/test_before_commit.py::test_checks_run_in_order_after_the_handler_and_before_commit`<br>`sandloom/core/tests/test_before_commit.py::test_subject_check_passes_without_a_mark_and_refuses_after_one`<br>`sandloom_tenancy/tests/test_accept_authority_at_commit.py::test_unchanged_session_accepts_and_commits`<br>`tests/integration/test_route_runtime.py::test_a_get_that_writes_is_rechecked_and_a_reading_get_is_not` |
| C-AUTH-08 | Billing operation intents with a periodic PSP reconciler (billing.tasks.reconcile\_operation\_intents) for class-F checkout / portal. | `sandloom_billing/tests/test_intent_reconcile.py::test_reconcile_task_body_reports_counts`<br>`sandloom_billing/tests/test_operation_intents.py::test_checkout_persists_an_issued_intent_keyed_to_the_psp_session` |
| C-AUTH-09 | current\_service is a service authenticator: it accepts the internal token (404 otherwise, also for a wrong token), resolves the route's auth as "service" and returns the service principal. | `tests/integration/test_request_boundaries.py::test_current_service_authenticates_a_service_principal` |

### C-ROUTE

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-ROUTE-01 | no\_idempotency(reason=) opts a write route out of the automatic idempotency guard; an explicit idempotency\_guard(...) keeps all its parameters (scope, durable, store\_response, fail\_closed) and its misuse checks. | `tests/integration/test_automatic_idempotency.py::test_without_a_request_unit_the_guard_is_the_fail_closed_in_flight_form`<br>`tests/integration/test_automatic_idempotency.py::test_mode_x_re_runs_authorization_and_the_handler`<br>`sandloom/core/tests/test_durable_idempotency_guard.py::test_durable_guard_mode_and_misuse` |
| C-ROUTE-02 | sensitive(risk) is the escape hatch for a hand-rolled authority check: it resolves {risk: ("sensitive()",)}; sensitive("security") derives a 300 s step-up; an unknown risk is refused. | `tests/integration/test_sensitive_risk.py::test_sensitive_marker_and_effect_tags_resolve_with_their_source`<br>`tests/architecture/test_operation_classes.py::test_unknown_class_is_refused` |
| C-ROUTE-03 | sandloom check untagged-sensitive (SANDLOOM-C022, warning) reports a money command without risk="money", a credential / session change (revocation, block, unblock) without @sensitive\_effect, and a write route that decides an authority by hand (an authority call or a 403 in its body, at\_commit(), a membership change behind a member-level gate) without sensitive(risk); a route behind a framework admin gate (route, parameter or router dependencies) or public() is not reported. On the framework tree the mixed-authority routes and owning administration effects declare admin risk; the shared identity session-revocation mechanism has one local, reasoned waiver because its caller owns the authorization and credential-step-up policy. | `tests/architecture/test_api10_checks.py::test_c022_untagged_money_and_security_effects`<br>`tests/architecture/test_api10_checks.py::test_c022_hand_rolled_authority_routes`<br>`tests/architecture/test_api10_checks.py::test_c022_bundled_operations_have_no_unreviewed_sensitive_effects` |

### C-UNIT

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-UNIT-01 | db() from a task that does not own the unit is SANDLOOM-R013; child tasks open their own unit with open\_uow(). | `tests/integration/test_units_and_admission.py::test_a_session_passed_to_another_task_is_detected`<br>`tests/integration/test_units_and_admission.py::test_child_db_before_the_parent_creates_nothing_then_parent_owns` |
| C-UNIT-02 | Best-effort footgun detection (detection, not a guarantee): with DB\_SESSION\_OWNER\_CHECKS on (the test environment's default; off in production, kept off the hot path by the performance gate) a session object used from another task is detected on its kernel-session methods and at the cursor; db() from a non-owner task is always refused (C-UNIT-01). Known undetectable forms: in-place mutation of a mutable column value (JSON) and attribute assignment on a loaded instance (no attribute listeners, kept off the hot path by the performance gate). | `tests/integration/test_units_and_admission.py::test_a_session_passed_to_another_task_is_detected`<br>`tests/integration/test_units_and_admission.py::test_in_place_json_mutation_is_a_known_undetectable_form` |

### C-SPLIT

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-SPLIT-01 | The same code deploys as a monolith or as any subset of identity/tenancy/billing purely via SANDLOOM\_MODULES + SERVICE\_BINDINGS: bound subsets answer the request script identically to the monolith, unbound ones identically or with an explicit 404/501/503 (never a 500 or a different 2xx), on both transports. | `tests/split_conformance/test_split_conformance.py::test_subset_conforms_to_monolith`<br>`tests/split_conformance/test_split_conformance.py::test_monolith_script_matches_expectations`<br>`tests/split_conformance/test_split_conformance.py::test_every_process_is_ready` |
| C-SPLIT-02 | core.get\_service resolves local-first: a co-deployed provider wins over a SERVICE\_BINDINGS proxy, otherwise a generated RPC proxy is returned (reused until the binding changes), and with neither it raises ServiceNotFound so consumers degrade explicitly. | `sandloom/core/tests/test_service_rpc.py::test_core_get_service_prefers_local_over_remote_binding`<br>`sandloom/core/tests/test_service_rpc.py::test_get_service_degrades_explicitly_when_unavailable`<br>`sandloom/core/tests/test_service_rpc.py::test_get_service_reuses_the_remote_proxy_until_the_binding_changes`<br>`sandloom/core/tests/test_contract_runtime.py::test_local_contributor_wins_over_remote_binding` |
| C-SPLIT-03 | Every bundled module subset (and each module alone through create\_app + lifespan) boots and becomes ready, refusing only with the documented ContractBindingError for a required consumed contract. | `sandloom/core/tests/test_module_subset.py::test_module_subset_boots_alone`<br>`sandloom/core/tests/test_module_subset.py::test_each_single_module_subset_is_ready`<br>`sandloom/core/tests/test_module_subset.py::test_full_monolith_set_loads`<br>`sandloom_identity/tests/test_module_wiring.py::test_module_boots_alone`<br>`sandloom_tenancy/tests/test_module_wiring.py::test_module_boots_alone`<br>`sandloom_billing/tests/test_module_wiring.py::test_module_boots_alone` |
| C-SPLIT-04 | Remote peers never feed /health; a down peer fails /ready only for a binding marked "required": true, and required probes run concurrently, each capped at SERVICE\_PROBE\_TIMEOUT\_SECONDS. | `sandloom/core/tests/test_split_readiness.py::test_required_peer_down_fails_ready_but_never_health`<br>`sandloom/core/tests/test_split_readiness.py::test_down_peer_does_not_fail_health_or_ready_by_default`<br>`sandloom/core/tests/test_split_readiness.py::test_required_probes_run_concurrently_capped_and_authenticated` |
| C-SPLIT-05 | SERVICE\_BINDINGS is strictly validated at startup: only base\_url/url (absolute http(s)), timeout\_seconds, critical, required and service\_id are accepted; unknown keys (typos) and an internal\_token are errors. | `sandloom/core/tests/test_split_readiness.py::test_invalid_service_binding_is_rejected`<br>`sandloom/core/tests/test_split_readiness.py::test_valid_service_bindings_accepted`<br>`sandloom/core/tests/test_rpc_allowlist.py::test_a_binding_carries_no_token` |
| C-SPLIT-06 | The shipped microservices profile binds exactly each service's consumed-but-not-local contracts (identity binds the remote claims contracts), derives RPC\_ALLOWED\_CALLS from those bindings, and every service keys its responses with callers pinning providers. | `sandloom/core/tests/test_deploy_profiles.py::test_microservices_bindings_are_exactly_the_consumed_contracts`<br>`sandloom/core/tests/test_deploy_profiles.py::test_token_issuer_binds_every_remote_claims_contributor`<br>`sandloom/core/tests/test_deploy_profiles.py::test_rpc_allowlist_is_derived_from_bindings`<br>`sandloom/core/tests/test_deploy_profiles.py::test_every_service_keys_its_responses_and_callers_pin_providers` |
| C-SPLIT-07 | A token issuer deployed alone collects other modules' claims over RPC through their .claims.v1 binding; a critical remote contributor failure aborts minting with 503. | `sandloom/core/tests/test_contract_runtime.py::test_identity_alone_collects_remote_claims`<br>`sandloom/core/tests/test_contract_runtime.py::test_critical_remote_contributor_failure_is_503` |

### C-RPC

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-RPC-01 | RPC\_TRANSPORT selects the shared outbound client: aiohttp (default, production) or httpx (fallback, for egress proxies); both serve calls, including TLS to a self-signed peer trusted via SSL\_CERT\_FILE. | `sandloom/core/tests/test_rpc_transports.py::test_rpc_transport_setting_selects_the_shared_client`<br>`sandloom/core/tests/test_rpc_transport_compat.py::test_the_fallback_client_serves_calls`<br>`sandloom/core/tests/test_rpc_transports.py::test_tls_self_signed_trusted_via_ssl_cert_file` |
| C-RPC-02 | The aiohttp client keeps a bounded per-peer keep-alive pool (RPC\_POOL\_MAX\_CONNECTIONS, waits bounded by the call timeout and observed) with idle eviction after RPC\_POOL\_IDLE\_SECONDS. | `sandloom/core/tests/test_rpc_transports.py::test_keep_alive_connection_is_reused`<br>`sandloom/core/tests/test_rpc_transports.py::test_pool_is_bounded_per_peer_and_waits_are_observed`<br>`sandloom/core/tests/test_rpc_transports.py::test_pool_wait_is_bounded_by_the_timeout`<br>`sandloom/core/tests/test_rpc_transports.py::test_idle_connections_are_evicted` |
| C-RPC-03 | `sandloom keys generate --purpose rpc --service <id>` emits a separate Ed25519 + X25519 pair (with RPC\_SERVICE\_KEYS / RPC\_SERVICE\_DH\_KEYS entries) that validates as a working configuration, and `--purpose rpc-dh` emits the X25519 pair only (optionally into --out-dir). | `sandloom/core/tests/test_rpc_allowlist.py::test_keys_generate_rpc_validates`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_keys_generate_rpc_dh` |
| C-RPC-04 | `sandloom keys allowlist --bindings <svc>='<SERVICE_BINDINGS>'` prints the RPC\_ALLOWED\_CALLS derived from the services' bindings. | `sandloom/core/tests/test_rpc_allowlist.py::test_allowlist_is_derived_from_service_bindings` |
| C-RPC-05 | sandloom.core.contract\_testing.assert\_contract\_conformance(core, name, \[ContractCase(...)\]) lets an app prove a contract answers identically locally and through a generated proxy (in-process ASGI or a real aiohttp/httpx socket), also under the pair MAC. | `sandloom/core/tests/test_contract_parity.py::test_local_and_rpc_results_and_errors_are_identical`<br>`sandloom/core/tests/test_rpc_pair_mac.py::test_contract_conformance_holds_under_service_mac` |

### C-INFRA

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-INFRA-01 | A new backend is a driver, not a core edit: inside the kernel, any provider implementing a contract plugs in through the registry (a non-SQLAlchemy database driver included), and a CapabilitySpec adds a capability. A driver package can do so for the contracts sandloom.driverkit publishes and for capabilities of its own. | `sandloom/core/tests/test_infra_multi_database.py::test_custom_non_sqlalchemy_database_driver`<br>`sandloom/core/tests/test_extra_capabilities.py::test_plugin_adds_capability_and_driver`<br>`sandloom/core/tests/test_capabilities.py::test_register_and_query_capability` |
| C-INFRA-02 | Driver packages are found by name in the sandloom.drivers entry-point group and imported only when INFRA selects them, so load order does not matter and an unselected package runs no code. A selected name with two providers, or one that cannot be used, stops startup (SANDLOOM-B021, B022). | `sandloom/core/tests/test_driverkit.py::test_a_selected_driver_is_found_by_name`<br>`sandloom/core/tests/test_driverkit.py::test_an_unselected_package_is_never_imported`<br>`sandloom/core/tests/test_driverkit.py::test_a_capability_in_another_package_loads_in_any_order`<br>`sandloom/core/tests/test_driverkit.py::test_two_providers_of_a_selected_name_are_refused`<br>`sandloom/core/tests/test_driverkit.py::test_a_package_cannot_take_over_a_built_in_driver`<br>`sandloom/core/tests/test_driverkit.py::test_a_selected_driver_that_cannot_be_used_stops_startup` |
| C-INFRA-03 | With no extra config one default instance per capability is synthesized from the flat settings; the INFRA JSON env var adds named instances or overrides drivers/options. | `sandloom/core/tests/test_infra_config.py::test_defaults_synthesized_for_every_capability`<br>`sandloom/core/tests/test_infra_config.py::test_infra_overlay_adds_named_instance`<br>`sandloom/core/tests/test_infra_config.py::test_infra_overlay_overrides_default_options`<br>`sandloom/core/tests/test_infra_config.py::test_new_instance_requires_driver` |
| C-INFRA-04 | Several databases run at once as independent engines/pools, each with its own UoW and its own transactional outbox; with several active sessions the target database must be named (database=...). | `sandloom/core/tests/test_infra_multi_database.py::test_two_databases_are_independent_and_both_work`<br>`sandloom/core/tests/test_post_commit_dispatcher.py::test_marks_are_coalesced_into_one_update_per_database`<br>`sandloom/core/tests/test_review_fixes_v2.py::test_resolve_scoped_session_ambiguous_without_database`<br>`sandloom/core/tests/test_testing_harness.py::test_bind_test_uow_targets_the_named_database` |
| C-INFRA-05 | Endpoints resolve infrastructure by logical name through DI: Annotated\[AsyncSession, Database("analytics")\] for a named database and Capability(cap, Contract, name) for any capability. | `sandloom/core/tests/test_uow_commits_before_response.py::test_dbsession_and_database_markers_use_function_scope`<br>`sandloom/core/tests/test_async_dependencies.py::test_dependency_factories_are_async`<br>`sandloom/core/tests/test_kernel_review_regressions.py::test_revocation_at_commit_covers_named_db_in_either_dependency_order`<br>**gap (S)**: no test resolves a Capability(...) dependency in a request and asserts it yields the named provider (or the contract/feature error) |
| C-INFRA-06 | document\_store, graph\_store, workflow, search, lock and secrets ship as CapabilitySpecs with contracts, marked stability=experimental, registered at bootstrap without a core driver; a plugin supplies the driver. | `sandloom/core/tests/test_extra_capabilities.py::test_extra_capabilities_are_specs`<br>`sandloom/core/tests/test_extra_capabilities.py::test_extra_capabilities_are_marked_experimental`<br>`sandloom/core/tests/test_extra_capabilities.py::test_known_capability_without_driver_gives_clear_error`<br>`sandloom/core/tests/test_extra_capabilities.py::test_capability_spec_rejects_unknown_stability` |
| C-INFRA-07 | notifier().send(Notification(...)) works with the zero-dependency log driver by default (never logging the raw recipient; no delivery features advertised in prod unless opted in) and with NOTIFIER\_BACKEND=smtp for email (STARTTLS verifies the certificate; login without TLS refused in prod). | `sandloom/core/tests/test_notifications.py::test_default_resolves_to_log_notifier`<br>`sandloom/core/tests/test_notifications.py::test_log_notifier_never_logs_the_raw_recipient`<br>`sandloom/core/tests/test_notifications.py::test_log_driver_advertises_no_delivery_features_in_prod`<br>`sandloom/core/tests/test_notifications.py::test_smtp_starttls_verifies_the_server_certificate` |
| C-INFRA-08 | The object store is key-addressed (put(key, data, content\_type)/get/delete/url): callers pass a logical key + bytes, never a host path; traversal and symlink escapes out of the root are rejected. | `sandloom/core/tests/test_infra_hardening.py::test_objectstore_roundtrip`<br>`sandloom/core/tests/test_infra_hardening.py::test_objectstore_rejects_traversal`<br>`sandloom/core/tests/test_infra_hardening.py::test_objectstore_rejects_symlink_escaping_root` |
| C-INFRA-09 | The cache capability is operation-based (get/set/add/delete/incr, cas compare\_and\_set/compare\_and\_delete, fused incr\_window/incr\_many/set\_nx\_get/set\_max) and never hands modules a raw client. | `sandloom/core/tests/test_infra_hardening.py::test_cache_provider_operations` |
| C-INFRA-10 | /diagnostics/infra reports configured capabilities, instances, stability, registered drivers and per-instance health (registry.describe()). | `sandloom/core/tests/test_infra_registry.py::test_describe_snapshot`<br>`sandloom/core/tests/test_capabilities.py::test_describe_includes_capabilities`<br>`sandloom/core/tests/test_extra_capabilities.py::test_describe_renders_stability`<br>`sandloom/core/tests/test_infra_registry.py::test_health_all_and_aclose_all`<br>**gap (S)**: no test calls the /diagnostics/infra endpoint itself (only registry.describe(); the route appears only in the operation-class map) |
| C-INFRA-11 | core.capability(cap, name) is the universal accessor and core.require\_capability also enforces features; the typed sugar (cache/database/objectstore/taskqueue/notifier) routes through it. | `sandloom/core/tests/test_capabilities.py::test_facade_capability_and_require_capability`<br>`sandloom/core/tests/test_infra_registry.py::test_resolves_and_caches_instances` |
| C-INFRA-12 | The relational driver contract of the unit machinery: LazyUnitOfWorkContract (open\_lazily / close) backs the lazy request unit (refused otherwise), the commit hooks ask DatabaseProvider.has\_pending\_changes(session) (no SQLAlchemy call in the kernel hooks), and the SQLAlchemy driver implements both and advertises write\_admission. Verified on PostgreSQL/SQLAlchemy only. | `sandloom/core/tests/test_relational_driver_contract.py::test_the_lazy_unit_needs_the_lazy_uow_contract`<br>`sandloom/core/tests/test_relational_driver_contract.py::test_the_commit_hooks_ask_the_driver_for_pending_changes`<br>`sandloom/core/tests/test_relational_driver_contract.py::test_the_sqlalchemy_driver_reports_unflushed_orm_changes`<br>`tests/integration/test_write_store.py::test_a_database_hosting_request_units_needs_write_admission` |

### C-MODULE

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-MODULE-01 | sandloom add module  generates a compliant, parseable module skeleton (ORM-free api, versioned event channels, TenantScoped example, declarative register), namespace-portable, refusing bad names and overwrites. | `sandloom/core/tests/test_module_generation.py::test_add_module_generates_a_compliant_parseable_skeleton`<br>`sandloom/core/tests/test_module_generation.py::test_add_module_refuses_bad_names_and_overwrites` |
| C-MODULE-02 | sandloom new  generates a consumer-owned app project (app/asgi on the kernel create\_app, deploy files, CI + pre-commit running sandloom check, a manifest enforced by sandloom check) whose own tests and lint pass out of the box. | `sandloom/core/tests/test_project.py::test_generate_minimal_project`<br>`sandloom/core/tests/test_project.py::test_generated_project_passes_its_own_tests`<br>`sandloom/core/tests/test_project.py::test_generated_manifest_is_enforced_by_sandloom_check`<br>`sandloom/core/tests/test_project.py::test_generated_ci_and_precommit_run_sandloom_check` |
| C-MODULE-03 | Packaging: a generated app depends on the sandloom distribution (compatible range, with its Redis/Celery drivers) and on one ecosystem package per module chosen with --with (sandloom-identity, ...: compatible ranges, no exact pin), every sandloom.\* import of a generated project ships in its declared distributions, and the app reuses the kernel factory. | `sandloom/core/tests/test_generated_project_packaging.py::test_generated_imports_ship_in_its_dependencies`<br>`sandloom/core/tests/test_project.py::test_apps_depend_on_sandloom_and_each_chosen_module_package`<br>`sandloom/core/tests/test_generated_project_packaging.py::test_minimal_app_uses_the_kernel_factory`<br>`sandloom/core/tests/test_project.py::test_asgi_reuses_framework_factory` |
| C-MODULE-04 | define\_module(**name**, setup=..., router=..., events=...) calls setup(core) for contracts/consumers/projections, accepts explicit router/events overrides, supports modules without api/events, and infers the package from the caller frame as a fallback. | `sandloom/core/tests/test_define_module.py::test_frame_inference_and_setup_hook`<br>`sandloom/core/tests/test_define_module.py::test_explicit_router_and_events_override_discovery`<br>`sandloom/core/tests/test_define_module.py::test_module_without_api_or_events` |
| C-MODULE-05 | sandloom check \[--only rule,...\] \[--list-rules\] runs the stable set of architecture analyzers against an architecture\_rules.yml manifest (framework or app), exiting 0 ok / 1 violations / 2 usage. | `tests/architecture/test_archcheck_library.py::test_rule_ids_are_stable`<br>`tests/architecture/test_archcheck_library.py::test_cli_help_lists_every_rule`<br>`sandloom/core/tests/test_project.py::test_sandloom_check_cli_exit_codes`<br>`tests/architecture/test_archcheck_library.py::test_cli_rejects_missing_module_directory`<br>rule `module-boundaries`<br>rule `framework-imports`<br>rule `raw-provider`<br>rule `infra-imports`<br>rule `kernel-handles`<br>rule `uow-boundary`<br>rule `best-effort-effects`<br>rule `get-core-await`<br>rule `endpoints-outside-api`<br>rule `orm-leakage`<br>rule `schema-orm-imports`<br>rule `test-imports` |
| C-MODULE-06 | SANDLOOM\_MODULES selects the module subset a process loads; every subset boots and becomes ready on its own, a blacklisted module is excluded, and a subset whose only module failed is not ready. | `sandloom/core/tests/test_module_subset.py::test_module_subset_boots_alone`<br>`sandloom/core/tests/test_module_subset.py::test_each_single_module_subset_is_ready`<br>`sandloom/core/tests/test_module_subset.py::test_blacklist_excludes_module`<br>`sandloom/core/tests/test_module_subset.py::test_subset_whose_only_module_failed_is_not_ready` |
| C-MODULE-07 | MODULE\_REQUIRED makes a module's load failure abort startup; when empty, every module listed explicitly in SANDLOOM\_MODULES is required, an explicit MODULE\_REQUIRED wins, and a required dependent of a failed module aborts. | `sandloom/core/tests/test_split_readiness.py::test_explicitly_listed_modules_are_required_by_default`<br>`sandloom/core/tests/test_split_readiness.py::test_explicit_module_required_wins`<br>`sandloom/core/tests/test_lifecycle_critical.py::test_entrypoint_fail_fast_honors_module_required`<br>`sandloom/core/tests/test_loader_migrations_hardening.py::test_required_dependent_of_failed_module_aborts` |
| C-MODULE-08 | The sandloom.core.testing pytest plugin, activated explicitly with addopts -p sandloom.core.testing.plugin (no pytest11 entry point), loads .env.test before any conftest imports settings, refuses a non-test database and provides the client/db/make\_headers fixtures, so app modules need no conftest. | `sandloom/core/tests/test_testing_plugin.py::test_env_test_is_loaded_before_any_conftest_imports_settings`<br>`sandloom/core/tests/test_testing_plugin.py::test_refuses_a_non_test_database`<br>`sandloom/core/tests/test_project.py::test_generated_app_activates_the_test_plugin`<br>`tests/architecture/test_ledger_static_pins.py::test_no_distribution_registers_a_pytest11_plugin` |
| C-MODULE-09 | migrations/env.py is run\_module\_migrations(**file**): the package, .models and the \_ table prefix are derived from the env.py location, keywords override, and a non-package location is a clear error. | `sandloom/core/tests/test_run_module_migrations.py::test_conventions`<br>`sandloom/core/tests/test_run_module_migrations.py::test_explicit_overrides`<br>`sandloom/core/tests/test_run_module_migrations.py::test_package_is_derived_from_the_env_file`<br>`sandloom/core/tests/test_run_module_migrations.py::test_not_a_package_is_a_clear_error` |
| C-MODULE-10 | A route policy's atoms add the contracts they resolve to the module's derived consumes as optional requirements (SubscriptionActive / Credits the entitlements contract, Entitled the fact owner's facts.v1, Subscribed billing.facts.v1, OrgRole the membership contract). | `tests/sdk/test_sdk_discovery.py::test_route_policies_add_the_contracts_their_atoms_call` |
| C-MODULE-11 | identity.contact.v1 answers a user's contact details (email, name, email\_verified, is\_active) by id, locally and over RPC alike, so a module that writes to users needs no PII in events and no identity edit. | `sandloom_identity/tests/test_contact_contract.py::test_get_contact_and_get_contacts`<br>`sandloom_identity/tests/test_contact_contract.py::test_contact_contract_conforms` |
| C-MODULE-12 | Module ecosystem: a module distribution registers its module in the sandloom.modules entry-point group (name = import path); its entry-point name is its module name (tables, migration chain, channels, contracts), SANDLOOM\_MODULES / MODULE\_REQUIRED / deploy profiles may name it by that name or by import path, and a requested name with two providers (two installed packages, or an installed package and a local module) is refused before anything loads (SANDLOOM-B020), naming both and the fix. | `sandloom/core/tests/test_module_names.py::test_module_name_is_the_entry_point_name_else_the_last_segment`<br>`sandloom/core/tests/test_module_names.py::test_entry_point_names_and_import_paths_resolve_alike`<br>`sandloom/core/tests/test_module_names.py::test_two_packages_providing_one_requested_name_are_refused`<br>`sandloom/core/tests/test_module_names.py::test_a_local_module_named_like_an_installed_one_is_refused`<br>`sandloom/core/tests/test_module_names.py::test_the_loader_refuses_an_ambiguous_module_before_loading_anything`<br>`sandloom/core/tests/test_module_names.py::test_configuration_names_installed_modules_by_entry_point_name`<br>`tests/sdk/test_sdk_module_names.py::test_the_sdk_owner_rule_agrees_with_the_kernel` |
| C-MODULE-13 | Distributions are layered and work installed alone: no module a distribution ships imports (module level, function-local or a constant-string dynamic import) a module of a distribution it does not depend on, nor a first-party module no distribution ships, nor an undeclared third-party package outside an ImportError guard; pip install sandloom alone (no module package, no Redis/Celery) runs every sandloom subcommand's --help, sandloom docs  and sandloom new; sandloom\[standard\] composes the installed modules; an app's sandloom check treats an installed module package as another module. | `tests/architecture/test_distribution_imports.py::test_a_kernel_import_of_a_module_package_is_caught`<br>`tests/architecture/test_distribution_imports.py::test_the_lazy_cli_import_of_an_absent_distribution_is_caught`<br>`tests/architecture/test_distribution_imports.py::test_an_unshipped_first_party_module_is_caught`<br>`tests/architecture/test_distribution_imports.py::test_undeclared_third_party_is_caught_unless_optional`<br>`tests/integration/test_package_distributions.py::test_every_distribution_installs_and_runs_alone`<br>`tests/architecture/test_archcheck_library.py::test_framework_imports_catch_from_sandloom_import_and_dynamic_aliases`<br>`tests/architecture/test_archcheck_library.py::test_test_imports_is_a_whitelist` |

### C-MIGRATE

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-MIGRATE-01 | sandloom migrate upgrade-all --apply applies the core chain to every hosting database and each loaded module's chain to its database. | `sandloom/core/tests/test_project.py::test_upgrade_all_on_freshly_generated_project`<br>`tests/integration/test_migrations_per_chain.py::test_migrations_cli_loads_modules_with_chains` |
| C-MIGRATE-02 | sandloom migrate upgrade-module  \--apply applies one module's chain to its database(s). | `tests/integration/test_migration_revision_cli.py::test_revision_autogenerate_and_upgrade_for_app_module` |
| C-MIGRATE-03 | sandloom migrate upgrade-core \[--database x\|--all-databases\] --apply \[--lock-timeout S\] applies the core infra chain to a named database. | `sandloom/core/tests/test_ops_cli.py::test_cli_lock_timeout_flag_reaches_the_lock`<br>**gap (S)**: Only flag plumbing is tested; no test runs upgrade-core --database  against a second database. |
| C-MIGRATE-04 | sandloom migrate revision  \-m msg \[--autogenerate\] creates a revision in that module's own chain (its migrations/, version table and database), for app modules via SANDLOOM\_COMPOSITION\_ROOT; an unknown module fails cleanly. | `tests/integration/test_migration_revision_cli.py::test_revision_autogenerate_and_upgrade_for_app_module`<br>`tests/integration/test_migration_revision_cli.py::test_revision_unknown_module_fails_cleanly` |
| C-MIGRATE-05 | sandloom migrate databases lists the configured database instances (default plus INFRA-declared ones). | `sandloom/core/tests/test_migrations_multidb.py::test_lists_default_database`<br>`sandloom/core/tests/test_migrations_multidb.py::test_lists_named_databases_from_infra`<br>`sandloom/core/tests/test_migrations_multidb.py::test_resolve_unknown_database_raises` |
| C-MIGRATE-06 | sandloom migrate extract-module moves one module's existing data out of a shared database into its own (fence, migrate target, copy, verify, outbox/inbox hand-off, group-owner transfer), enabling monolith -\> microservices with data. | `tests/extraction/test_extraction_e2e.py::test_monolith_to_microservices_with_data`<br>`sandloom/core/tests/test_module_extraction.py::test_extract_crash_resume_verify_and_cleanup`<br>`sandloom/core/tests/test_module_extraction.py::test_spec_owns_exactly_the_module_tables` |
| C-MIGRATE-07 | sandloom migrate ensure-outbox \[--all-databases\] --apply idempotently creates the shared core-infra tables for dev/repair. | `sandloom/core/tests/test_migrations_multidb.py::test_ensure_outbox_creates_table_idempotently` |
| C-MIGRATE-08 | A module's migrations/env.py is the one-liner run\_module\_migrations(**file**), deriving package, table prefix and version table by convention with explicit overrides. | `sandloom/core/tests/test_run_module_migrations.py::test_conventions`<br>`sandloom/core/tests/test_run_module_migrations.py::test_explicit_overrides`<br>`sandloom/core/tests/test_run_module_migrations.py::test_package_is_derived_from_the_env_file` |
| C-MIGRATE-09 | sandloom migrate new autogenerates against a scratch database with every chain at head, so it needs no configured database at head (--against-database keeps that comparison) and loads the project's .env.test like sandloom test. | `tests/integration/test_migration_revision_cli.py::test_new_autogenerates_against_a_scratch_database` |

### C-PROJ

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-PROJ-01 | monotonic\_upsert / monotonic\_update apply a projection change only when the stored version is NULL or older than the incoming one, so at-least-once / out-of-order delivery never rolls a row back (tombstones block resurrection, and a tombstone purge never deletes one between an apply's watermark check and its write, as the apply holds the watermark row FOR SHARE until it commits). | `sandloom/core/tests/test_projection_upsert_guards.py::test_null_stored_version_is_updatable`<br>`sandloom/core/tests/test_projection_upsert_guards.py::test_tombstone_prevents_old_rebuild_from_resurrecting_deleted_entity`<br>`sandloom/core/tests/test_projections.py::test_consumer_applies_and_is_monotonic_and_idempotent`<br>`tests/sdk/test_sdk_projection_purge_race.py::test_a_purge_racing_an_apply_never_resurrects_a_relational_row` |
| C-PROJ-02 | Projection rows carry tenant\_id: monotonic\_upsert requires it and raises TenantMismatchError if it differs from the bound tenant; updates are scoped to the bound tenant. | `sandloom/core/tests/test_projection_upsert_guards.py::test_upsert_requires_tenant_and_never_crosses_tenants`<br>`sandloom/core/tests/test_projection_upsert_guards.py::test_update_is_scoped_to_bound_tenant` |
| C-PROJ-03 | sandloom projection list \| rebuild  \[--tenant \] rebuilds a projection from source modules' read contracts, running startup tasks only (no consumers, no relay). | `sandloom/core/tests/test_projections.py::test_rebuild_cli_does_not_start_consumers_or_relay`<br>`examples/projecthub/projecthub/modules/taskboard/tests/test_projection.py::test_rebuild_refreshes_stale_project_fields`<br>**gap (S)**: --tenant scoping of a rebuild is not exercised by a test. |
| C-PROJ-04 | monotonic\_upsert(insert\_only=...) seeds columns this source does not own on INSERT and never touches them on UPDATE. | **gap (S)**: No test (framework or examples) uses insert\_only; a unit test proving INSERT seeds and UPDATE preserves the column is missing. |
| C-PROJ-05 | A read module registers a QueryProjectionDescriptor, per-channel ProjectionConsumerDescriptors (durable consumers namespaced by the module) and a ProjectionRebuilder; a consumer for an unknown projection or a duplicate registration is rejected. | `sandloom/core/tests/test_projections.py::test_registry_register_and_duplicate`<br>`sandloom/core/tests/test_projections.py::test_module_registration_rejects_consumer_for_unknown_projection`<br>`sandloom/core/tests/test_projections.py::test_projection_consumer_group_is_namespaced_by_module`<br>`sandloom/core/tests/test_projections.py::test_start_projection_consumers_wires_and_delivers` |
| C-PROJ-06 | The projecthub taskboard pattern (version-guarded local source snapshot locked by both writers) keeps a denormalised field correct when a source event races a projection insert, and the projection is served by one query. | `examples/projecthub/projecthub/modules/taskboard/tests/test_projection.py::test_project_update_racing_a_task_insert_is_not_lost`<br>`examples/projecthub/projecthub/modules/taskboard/tests/test_projection.py::test_project_update_waits_for_an_uncommitted_task_insert`<br>`examples/projecthub/projecthub/modules/taskboard/tests/test_projection.py::test_projection_converges_and_is_served_by_one_query` |
| C-PROJ-07 | Experimental in 1.0 (owner decision 2026-10-26; relational model= targets are the supported 1.0 path). Projection(store=("document\_store", name)) targets a store whose driver advertises conditional\_write (put\_if\_newer / tombstone\_if\_newer, refused otherwise) and is read by View(...); the in-memory document\_store driver (memory) advertises conditional\_write and tenant\_native and passes the conformance kit. | `tests/sdk/test_sdk_projection_ops.py::test_a_store_projection_converges_and_reads_skip_tombstones`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_store_without_conditional_writes_is_refused`<br>`sandloom/core/tests/test_projection_kernel.py::test_the_memory_driver_advertises_and_passes_the_kit` |
| C-PROJ-08 | rebuild(name, tenant\_id=) and sandloom projection rebuild run the projection's rebuilder with the rebuild marked in its watermark row (purges refuse meanwhile), without the consumer runtime; rebuild\_all covers the tenants the projection holds, rebuild\_entity(":") one aggregate, rebuild\_from may name (contract, method); blue\_green=True runs in place (no 1.0 store swaps an alias) and says so in the result. | `sandloom/core/tests/test_projections.py::test_rebuild_cli_does_not_start_consumers_or_relay`<br>`tests/sdk/test_sdk_projection_ops.py::test_rebuild_entity_applies_one_aggregate_and_checks_its_ref`<br>`tests/sdk/test_sdk_projection_ops.py::test_a_blue_green_rebuild_runs_in_place_and_says_so` |
| C-PROJ-09 | A projection source group names its tenant field (tenant\_from=, e.g. organization\_id) and the consumer binds that tenant; a source whose version field is optional or which lacks the tenant field is refused at definition time with the alternative. | `tests/sdk/test_sdk_projections.py::test_a_source_names_its_tenant_field`<br>`tests/sdk/test_sdk_projections.py::test_an_optional_version_is_refused_with_the_alternative` |

### C-MSTORE

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-MSTORE-01 | Delivery lattice laws (sandloom.core.guarantee\_lattice): a guarantee's delivery set is closed under atomic =\> no\_loss and no\_dup; the meet is idempotent, commutative and associative, FULL is its unit on deliveries, and unknown properties are refused. | `sandloom/core/tests/test_guarantee_lattice.py::test_atomic_implies_no_loss_and_no_dup`<br>`sandloom/core/tests/test_guarantee_lattice.py::test_the_meet_is_a_semilattice_and_never_strengthens`<br>`sandloom/core/tests/test_guarantee_lattice.py::test_unknown_properties_are_refused` |
| C-MSTORE-02 | Monotonicity: combining effects (the meet) never raises a guarantee above either operand. | `sandloom/core/tests/test_guarantee_lattice.py::test_the_meet_is_a_semilattice_and_never_strengthens` |
| C-MSTORE-03 | Labels are derived from the mechanism, one scenario per row of the label table: only a write-store write is atomic; an outbox event is at-least-once (no\_loss only with a durable bus); a durable consumer effect is no\_dup only with the inbox in its database and the inbox outliving the stream; an effect in another store is no\_dup only through a conditional write; a command is no\_dup only with a database command log whose TTL covers the retries (never the cache fallback); consumer delivery is always qualified (parks\_\*); a label is shown only where it deviates. | `sandloom/core/tests/test_guarantee_lattice.py::test_each_row_of_the_label_table`<br>`sandloom/core/tests/test_guarantee_lattice.py::test_a_label_is_shown_only_when_it_deviates` |
| C-MSTORE-04 | A conditional\_write store passes conditional\_write\_conformance (older and duplicate versions never apply, tombstones are versioned and never resurrected, tenants isolated); the in-memory driver passes, a last-write-wins store fails. | `sandloom/core/tests/test_projection_kernel.py::test_the_memory_document_store_passes_the_conformance_kit`<br>`sandloom/core/tests/test_projection_kernel.py::test_the_conformance_kit_catches_a_store_without_the_version_guard`<br>`sandloom/core/tests/test_projection_kernel.py::test_the_memory_driver_advertises_and_passes_the_kit` |

### C-OPS

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-OPS-01 | sandloom outbox list-dead \| requeue \[--database\] \[--channel\] \[--id\|--all\] lists dead-lettered outbox rows and returns them to pending (the admin API requeue is audited atomically and idempotent). | `sandloom/core/tests/test_outbox_relay_backoff.py::test_requeue_dead_returns_rows_to_pending`<br>`sandloom/core/tests/test_admin_operations.py::test_outbox_requeue_is_audited_atomically_and_idempotent` |
| C-OPS-02 | Delivered outbox rows older than OUTBOX\_RETENTION\_HOURS are purged by the relay on a time cadence and on demand by sandloom outbox purge. | `sandloom/core/tests/test_outbox_relay_batch.py::test_retention_purge_deletes_only_old_delivered_rows`<br>`sandloom/core/tests/test_outbox_relay_purge_cadence.py::test_purge_runs_on_a_time_cadence_not_a_tick_count`<br>`sandloom/core/tests/test_outbox_relay_purge_cadence.py::test_purge_stops_at_its_time_budget`<br>**gap (S)**: The `sandloom outbox purge` CLI entry point itself is not invoked by a test. |
| C-OPS-03 | sandloom streams dlq-list \| dlq-replay  \[--id\] \[--group\] inspects and replays Streams dead letters, the replay targeted at the group that dead-lettered the entry. | `sandloom/core/tests/test_streams_split_identity.py::test_dlq_replay_reaches_only_the_failed_group`<br>`sandloom/core/tests/test_streams_split_identity.py::test_memory_dlq_replay_is_targeted_too`<br>`sandloom/core/tests/test_transport_redis.py::test_dlq_replay_republishes_and_clears` |
| C-OPS-04 | sandloom streams groups  \| consumers \[--all\] \| release-group   report group lag/pending, live consumers (flagging starved channel versions) and release a group. | `sandloom/core/tests/test_streams_split_identity.py::test_streams_cli_operations`<br>`sandloom/core/tests/test_streams_split_identity.py::test_inventory_lists_live_consumers_and_flags_starved_versions`<br>`sandloom/core/tests/test_streams_split_identity.py::test_group_lag_and_pending_are_reported` |
| C-OPS-05 | sandloom contracts snapshot \[--write\|--check\] / sandloom contracts check --against  exist for rolling-upgrade compatibility (owned by area D). | `sandloom/core/tests/test_contract_compat.py::test_committed_module_snapshots_match_registered_contracts`<br>`sandloom/core/tests/test_contract_compat.py::test_cli_check_against_snapshot` |
| C-OPS-06 | sandloom backup plan lists every database and the security Redis to back up without secrets; sandloom backup verify  restores a dump into a scratch database and checks it against code heads. | `sandloom/core/tests/test_backup_cli.py::test_plan_covers_databases_and_security_redis_without_secrets`<br>`sandloom/core/tests/test_backup_cli.py::test_verify_passes_for_a_current_restore_and_drops_the_scratch_db`<br>`sandloom/core/tests/test_backup_cli.py::test_real_pg_dump_round_trip` |
| C-OPS-07 | sandloom backup reconcile-security and sandloom security audit-export re-derive blocks/revocations from the audit log and outbox (newest record wins, a tie stays blocked) and flag a too-short outbox retention as a coverage gap. | `sandloom/core/tests/test_security_recovery.py::test_audit_export_restores_blocks_a_restored_database_lost`<br>`sandloom/core/tests/test_security_recovery.py::test_newest_record_wins_and_a_tie_is_blocked`<br>`sandloom/core/tests/test_security_recovery.py::test_short_outbox_retention_is_flagged_as_a_coverage_gap` |
| C-OPS-08 | sandloom security revoke-sessions \| force-password-reset (--all\|--tenant ORG\|--user ID) apply incident response in keyed, deduplicated batches through the owners' contracts; a forced reset refuses password login until reset. | `sandloom/core/tests/test_security_cli.py::test_apply_all_users_in_keyed_batches`<br>`sandloom/core/tests/test_security_cli.py::test_tenant_targets_page_through_membership`<br>`sandloom/core/tests/test_security_cli.py::test_missing_contract_is_an_explicit_error`<br>`sandloom_identity/tests/api/test_security_recovery_ops.py::test_forced_password_reset_refuses_password_login_until_reset` |
| C-OPS-09 | sandloom config dump \[--json\] \[--only PREFIXES\] prints effective settings with each value's source (env / dotenv / secrets\_dir / default). | `sandloom/core/tests/test_ops_cli.py::test_effective_config_reports_sources`<br>`sandloom/core/tests/test_ops_cli.py::test_config_dump_cli` |
| C-OPS-10 | sandloom doctor is a read-only preflight (config, modules, infra reachability, migration heads, consumer lag, RLS, strict profile) exiting 0 ok / 1 warnings / 2 failures, rejecting unknown sections. | `sandloom/core/tests/test_ops_cli.py::test_doctor_exit_codes`<br>`sandloom/core/tests/test_ops_cli.py::test_doctor_reports_each_section_and_exit_code`<br>`sandloom/core/tests/test_ops_cli.py::test_doctor_reports_a_broken_composition_root`<br>`sandloom/core/tests/test_ops_cli.py::test_doctor_flags_strict_profile_problems` |
| C-OPS-11 | sandloom keys generate \[--purpose rpc\] produces usable JWT / RPC key pairs, optionally as SECRETS\_DIR files, refusing to overwrite existing files and writing private keys with private permissions. | `sandloom/core/tests/test_asymmetric_jwt.py::test_keys_generate_prints_a_usable_pair`<br>`sandloom/core/tests/test_asymmetric_jwt.py::test_keys_generate_writes_secrets_dir_files`<br>`sandloom/core/tests/test_rpc_allowlist.py::test_keys_generate_rpc_validates`<br>`sandloom/core/tests/test_review_tooling_regressions.py::test_key_generation_refuses_existing_files_without_partial_rotation` |
| C-OPS-12 | Migration status compares each database's applied revisions with the code heads and reports unreachable databases without leaking secrets. | `sandloom/core/tests/test_ops_cli.py::test_migration_status_compares_applied_with_code_heads`<br>`sandloom/core/tests/test_ops_cli.py::test_migration_status_reports_unreachable_databases_without_secrets` |
| C-OPS-13 | Static CLIs (sandloom check, sandloom add) run with an empty environment without building Settings. | `sandloom/core/tests/test_static_cli_no_env.py::test_static_cli_runs_with_empty_environment`<br>`sandloom/core/tests/test_static_cli_no_env.py::test_sandloom_check_runs_against_a_manifest_with_empty_environment`<br>`sandloom/core/tests/test_static_cli_no_env.py::test_importing_static_submodules_does_not_build_settings` |
| C-OPS-14 | SECRETS\_DIR supplies any setting as a file named after its field (Docker/K8s file-based secrets). | `sandloom/core/tests/test_audit_and_secrets.py::test_secrets_dir_reads_file` |

### C-RISK

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-RISK-01 | SANDLOOM\_ACCEPTED\_RISKS (JSON list or comma list) names accepted risk ids and checks consult settings.accepts\_risk(id). | `sandloom/core/tests/test_config_tiers.py::test_accepted_risks_parse_json_or_comma_list`<br>`sandloom/core/tests/test_password_hash_config.py::test_prod_accepts_presets_and_the_explicit_opt_out` |

### C-DEPLOY

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-DEPLOY-01 | deploy/compose/monolith.yml runs every module in one app process plus worker and beat on one database, every process env validating, with an explicit BILLING\_PROVIDER. | `sandloom/core/tests/test_deploy_profiles.py::test_every_process_env_validates`<br>`sandloom/core/tests/test_deploy_profiles.py::test_billing_provider_must_be_chosen`<br>`sandloom/core/tests/test_deploy_profiles.py::test_worker_and_beat_use_framework_entrypoint` |
| C-DEPLOY-02 | deploy/compose/microservices.yml runs one module per service behind an nginx gateway, each with its own database and migrate one-shot, wired by SERVICE\_BINDINGS that are exactly the consumed-but-not-local contracts, with RPC allowlists derived from the bindings. | `sandloom/core/tests/test_deploy_profiles.py::test_microservices_db_per_service_and_bindings`<br>`sandloom/core/tests/test_deploy_profiles.py::test_microservices_bindings_are_exactly_the_consumed_contracts`<br>`sandloom/core/tests/test_deploy_profiles.py::test_gateway_routes_every_module_path_to_its_service`<br>`sandloom/core/tests/test_deploy_profiles.py::test_rpc_allowlist_is_derived_from_bindings` |
| C-DEPLOY-03 | The microservices stack boots end to end as real processes (split deployment smoke). | `tests/e2e/test_split_deployment.py::test_split_deployment_end_to_end`<br>`tests/e2e/test_split_deployment.py::test_required_mode_fails_instead_of_skipping` |
| C-DEPLOY-04 | Deploy profiles fit the Postgres connection budget, use one named image for every built service, a durable Redis bus, and (opt-in) a shared, durable idempotency Redis. | `sandloom/core/tests/test_deploy_profiles.py::test_connection_budget_fits_max_connections`<br>`sandloom/core/tests/test_deploy_profiles.py::test_one_named_image_for_every_built_service`<br>`sandloom/core/tests/test_deploy_profiles.py::test_redis_bus_is_persistent`<br>`sandloom/core/tests/test_deploy_profiles.py::test_idempotency_cache_opt_in_is_shared_and_durable` |
| C-DEPLOY-05 | The image serves via gunicorn + maintained uvicorn workers with a cgroup-aware default worker count (WEB\_CONCURRENCY overrides) and LB-safe keep-alive. | `sandloom/core/tests/test_gunicorn_conf.py::test_gunicorn_worker_count_honours_cgroup_quota`<br>`sandloom/core/tests/test_gunicorn_conf.py::test_gunicorn_uses_maintained_uvicorn_worker_with_lb_safe_keepalive` |
| C-DEPLOY-06 | sandloom add deploy  \--service  deploys a module whose required cross-module contracts are not bound yet (it reads the module's topology without the startup consumes preflight, which stays strict for applications), binds them to their providers' services, and gives a module with periodic tasks its \-worker / \-beat pair. | `sandloom/core/tests/test_scaffold_deploy.py::test_a_split_app_passes_the_framework_split_invariants`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_a_module_with_periodic_tasks_gets_its_worker_and_one_beat`<br>`sandloom/core/tests/test_consumes_preflight.py::test_topology_read_records_a_missing_required_contract` |
| C-DEPLOY-07 | An sandloom new application splits into microservices with no code change. sandloom add deploy ... --service  creates the app's microservices profile, nginx gateway and database init script from templates shipped in the package, then derives each service (its own database and migrate one-shot, RPC keys, worker and one beat), SERVICE\_BINDINGS (plus the issuer's remote claims), RPC\_ALLOWED\_CALLS and the gateway routes from what the modules provide and consume. A repeated run changes nothing, a module is added to a service incrementally, hand edits outside the generated blocks are kept and an edit inside one is refused with a diff. The framework's own profile is the same generator's output. Route collection covers every route a service's app serves (round 3) - root-level routes (/ and /{slug} of module\_router(prefix="")) get location = / and the catch-all location / (the internal surfaces then answering 404 explicitly), two services claiming the same route prefix, / or root-level parameter paths are refused with SANDLOOM-T014 naming them, never silently dropped, and a static catch-all left outside the generated routes block is refused. Each route prefix's locations stop at its segment boundary as Starlette matches (round 4) - location = /shop (to the service whose routes match the bare prefix) and location /shop/, never an unbounded location /shop, so /shopping stays with a root-level /{slug} service. | `sandloom/core/tests/test_scaffold_deploy.py::test_first_deploy_creates_the_profile_gateway_and_init_script`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_a_split_app_passes_the_framework_split_invariants`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_rerunning_is_idempotent`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_adding_a_module_is_incremental_and_keeps_hand_edits`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_a_hand_edit_inside_a_generated_block_is_refused_with_a_diff`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_the_framework_profile_is_the_generators_output`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_dev_env_covers_every_required_variable`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_a_root_level_module_gets_the_root_and_the_catch_all`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_two_services_claiming_root_level_paths_conflict`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_a_hand_written_catch_all_outside_the_block_is_refused`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_a_route_prefix_matches_only_its_own_segment`<br>`tests/e2e/test_app_split.py::test_an_sandloom_new_app_splits_into_microservices` |
| C-DEPLOY-08 | A microservices profile is checked against the split invariants the framework's own profile is pinned to (SANDLOOM-T014), in sandloom test's schema &amp; deploy step for an application's profile too, and every HTTP service boots alone with exactly its environment (settings, startup preflight, composition root, every consumed contract bound) without network. The gateway check compares the gateway with every route each service's app serves (root-level / and /{slug} included), never a prefix-filtered list (round 3), and samples adversarial paths (round 4) - each path parameter also takes every literal segment a route has at that position, bare and suffixed - requiring the upstream to be the service whose app routes match the path (Starlette matching, the most literal route winning). | `sandloom/core/tests/test_deploy_profiles.py::test_the_profile_passes_the_split_invariants_apps_are_checked_with`<br>`sandloom/core/tests/test_deploy_profiles.py::test_the_split_invariants_report_what_breaks_them`<br>`sandloom/core/tests/test_deploy_profiles.py::test_sandloom_test_checks_the_split_profile_of_a_project`<br>`sandloom/core/tests/test_deploy_profiles.py::test_every_service_boots_alone_with_its_bindings`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_the_checker_finds_a_root_route_the_gateway_drops`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_the_checker_samples_paths_that_collide_with_another_prefix` |
| C-DEPLOY-09 | A service added to a running microservices stack gets its database although the Postgres volume exists (init-databases.sh runs only on an empty volume) - every generated migrate- one-shot runs sandloom migrate upgrade-all --apply --create-databases, which creates each hosting PostgreSQL database when missing (idempotent, with the citext and pg\_trgm extensions), also in a profile generated before (the generated block carries the command). Race-safe (round 3) - provisioning is serialized per target name by an advisory lock on the maintenance database, and every caller ensures the extensions inside it whether or not it created the database, so a concurrent caller never returns (and migrates) before the database is complete. | `sandloom/core/tests/test_migrate_creates_database.py::test_a_missing_database_is_created_once_with_its_extensions`<br>`sandloom/core/tests/test_migrate_creates_database.py::test_a_concurrent_caller_never_sees_an_uninitialized_database`<br>`sandloom/core/tests/test_migrate_creates_database.py::test_an_existing_database_and_a_non_postgres_url_are_left_alone`<br>`sandloom/core/tests/test_migrate_creates_database.py::test_upgrade_all_takes_the_create_databases_flag`<br>`sandloom/core/tests/test_scaffold_deploy.py::test_a_service_added_later_creates_its_own_database`<br>`sandloom/core/tests/test_deploy_profiles.py::test_microservices_db_per_service_and_bindings`<br>`tests/e2e/test_app_split.py::test_an_sandloom_new_app_splits_into_microservices` |

### C-OBS

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-OBS-01 | Logs are structured JSON with request/trace context ids, JSON automatically in prod (LOG\_FORMAT=auto), and logging init is idempotent across create\_app calls. | `sandloom/core/tests/test_observability.py::test_log_as_json_follows_environment`<br>`sandloom/core/tests/test_observability.py::test_json_formatter_emits_structured_line`<br>`sandloom/core/tests/test_observability.py::test_context_filter_injects_ids`<br>`sandloom/core/tests/test_observability.py::test_init_logging_is_idempotent_across_create_app_calls` |
| C-OBS-02 | Prometheus RED metrics are recorded per request, every safe\_inc name resolves to a defined metric, and gauges declare a multiprocess mode so they render under gunicorn. | `sandloom/core/tests/test_observability.py::test_red_metrics_recorded_on_request`<br>`sandloom/core/tests/test_metrics_integrity.py::test_every_safe_inc_name_resolves_to_a_metric`<br>`sandloom/core/tests/test_operational_metrics.py::test_every_gauge_declares_a_live_multiprocess_mode`<br>`sandloom/core/tests/test_operational_metrics.py::test_new_gauges_render_from_the_multiprocess_directory` |
| C-OBS-03 | Outbox oldest-pending age (ignoring delivered/dead rows, refreshed even while the broker circuit is open) and Streams oldest-unacked age per group are exported. | `sandloom/core/tests/test_operational_metrics.py::test_outbox_oldest_pending_age_ignores_delivered_and_dead`<br>`sandloom/core/tests/test_operational_metrics.py::test_relay_tick_refreshes_age_while_the_broker_circuit_is_open`<br>`sandloom/core/tests/test_operational_metrics.py::test_streams_oldest_unacked_age_from_entry_ids`<br>`sandloom/core/tests/test_operational_metrics.py::test_heartbeat_sets_the_group_oldest_age_gauge` |
| C-OBS-04 | Pool, rate-limiter, revocation-check, RPC-client and consumer latency metrics are exported per mode/peer/outcome. | `sandloom/core/tests/test_operational_metrics.py::test_pool_sampler_reports_checked_out_idle_and_capacity`<br>`sandloom/core/tests/test_operational_metrics.py::test_rate_limiter_counts_rejections_and_cache_outages`<br>`sandloom/core/tests/test_operational_metrics.py::test_revocation_check_latency_by_mode`<br>`sandloom/core/tests/test_operational_metrics.py::test_rpc_client_latency_per_peer_and_outcome` |
| C-OBS-05 | OTLP tracing is configured from TRACING\_URL (off without it); a span setup failure degrades to a no-op span and body exceptions propagate unchanged. | `sandloom/core/tests/test_tracing.py::test_tracing_stays_off_without_tracing_url`<br>`sandloom/core/tests/test_tracing.py::test_tracing_is_configured_from_settings`<br>`sandloom/core/tests/test_tracing.py::test_otlp_http_uses_http_exporter_and_flushes_once`<br>`sandloom/core/tests/test_tracing.py::test_span_setup_failure_degrades_to_a_noop_span` |
| C-OBS-06 | SENTRY\_DSN enables error reporting (no-op when unset) with a before\_send scrubber. | `sandloom/core/tests/test_ops_security_hardening.py::test_sentry_before_send_scrubs_event`<br>**gap (S)**: No test asserts Sentry init is a no-op without SENTRY\_DSN / initialises with it. |
| C-OBS-07 | GC tuning (GC\_FREEZE\_AFTER\_STARTUP / GC\_THRESHOLDS / GC\_METRICS\_ENABLED) freezes the startup heap and applies thresholds in gunicorn and Celery workers (entry points only, never create\_app), with a pause histogram. | `sandloom/core/tests/test_gc_tuning.py::test_gc_settings_defaults`<br>`sandloom/core/tests/test_gc_tuning.py::test_post_worker_init_wraps_the_app_and_keeps_attributes`<br>`sandloom/core/tests/test_gc_tuning.py::test_celery_worker_init_freezes_in_the_parent`<br>`sandloom/core/tests/test_gc_tuning.py::test_gc_metrics_buffer_flushes_into_prometheus` |
| C-OBS-08 | The shipped alert/dashboard bundle references only existing metrics and labels, every alert has severity and a runbook entry matching docs/operate/operator-reference.md, and promtool rule tests pass. | `sandloom/core/tests/test_observability_bundle.py::test_every_referenced_metric_and_label_exists`<br>`sandloom/core/tests/test_observability_bundle.py::test_alerts_have_severity_and_runbook`<br>`sandloom/core/tests/test_observability_bundle.py::test_operations_alert_index_matches_rules`<br>`sandloom/core/tests/test_observability_bundle.py::test_promtool_rules_and_unit_tests` |

### C-CONSOLE

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-CONSOLE-01 | The sandloom-admin wheel carries the build of the current console source and no application data, so an application serves it with neither Node nor a separate image. | `tests/architecture/test_admin_console_package.py::test_the_packaged_console_is_built_from_the_current_source`<br>`tests/architecture/test_admin_console_package.py::test_the_packaged_console_ships_no_application_data` |
| C-CONSOLE-02 | The console is off in production unless ADMIN\_CONSOLE\_ENABLED is set, forbids foreign code and framing, and never answers a missing asset with HTML. | `sandloom_admin/tests/test_console.py::test_production_keeps_it_off_unless_enabled`<br>`sandloom_admin/tests/test_console.py::test_the_entry_page_forbids_foreign_code`<br>`sandloom_admin/tests/test_console.py::test_a_missing_file_is_not_answered_with_html` |
| C-CONSOLE-03 | sandloom generate writes the console's manifest and refuses a declaration the application's API does not support; a console that cannot show the manifest says why instead of breaking. | `sandloom/core/tests/test_generated_admin_manifest.py::test_a_bad_declaration_is_a_problem_not_a_crash`<br>`sandloom_admin/tests/test_console.py::test_a_newer_manifest_version_is_refused`<br>`sandloom_admin/tests/test_console.py::test_a_widget_this_console_lacks_is_refused` |
| C-CONSOLE-04 | sandloom security grant-admin makes a registered user a platform administrator, verifies the address, keeps one assignment when repeated and writes a required audit row. | `sandloom_identity/tests/api/test_security_recovery_ops.py::test_appointing_the_first_admin_grants_verifies_and_audits`<br>`sandloom_identity/tests/api/test_security_recovery_ops.py::test_appointing_twice_keeps_one_assignment` |

### C-TEST

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-TEST-01 | The sandloom.core.testing pytest plugin (-p sandloom.core.testing.plugin) loads .env.test before any settings import, honours a composition-root ini option and exposes the db/client/db\_uow fixtures binding service effects to the test UoW. | `sandloom/core/tests/test_testing_plugin.py::test_plugin_exposes_db_uow_and_binds_service_effects`<br>`sandloom/core/tests/test_testing_plugin.py::test_env_test_is_loaded_before_any_conftest_imports_settings`<br>`sandloom/core/tests/test_testing_plugin.py::test_composition_root_ini_option`<br>`sandloom/core/tests/test_testing_plugin.py::test_missing_env_test_is_a_clear_usage_error` |
| C-TEST-02 | Under xdist each worker gets its own database \_gw (created on first use) and Redis DB indexes offset by a configurable or controller-allocated stride. | `sandloom/core/tests/test_testing_workers.py::test_worker_gets_its_own_database_and_redis_dbs`<br>`sandloom/core/tests/test_testing_workers.py::test_stride_is_configurable`<br>`sandloom/core/tests/test_testing_workers.py::test_auto_stride_uses_the_controllers_allocation`<br>`sandloom/core/tests/test_testing_workers.py::test_ensure_database_creates_the_worker_database` |
| C-TEST-03 | SANDLOOM\_TEST\_DATABASE\_URL / SANDLOOM\_TEST\_CACHE\_URL / SANDLOOM\_TEST\_BROKER\_URL win over .env.test on every load path (and are rewritten per xdist worker); reload\_test\_env reapplies the rewrite. | `sandloom/core/tests/test_testing_workers.py::test_explicit_overrides_win_over_env_test_on_every_load_path`<br>`sandloom/core/tests/test_testing_workers.py::test_overrides_are_rewritten_per_xdist_worker`<br>`sandloom/core/tests/test_testing_workers.py::test_reload_test_env_reapplies_the_worker_rewrite` |
| C-TEST-05 | tools/checks/test\_resources.py list\|prune manages shared test DBs / Redis indexes: prune is a dry run without --apply and never touches a connected or recently used resource. | `sandloom/core/tests/test_testing_resources.py::test_only_an_old_disconnected_marked_resource_is_stale`<br>`sandloom/core/tests/test_testing_resources.py::test_the_age_threshold_never_drops_below_a_day`<br>`sandloom/core/tests/test_testing_resources.py::test_scan_and_prune_a_stale_index_but_never_a_changed_one`<br>`sandloom/core/tests/test_testing_resources.py::test_a_database_is_stamped_on_claim_and_never_dropped_while_connected` |
| C-TEST-06 | Unit-test UoW helpers recording\_uow() and bind\_test\_uow(session, database=) let tests capture required audits / effects without a real request. | `sandloom/core/tests/test_testing_harness.py::test_recording_uow_captures_required_audits`<br>`sandloom/core/tests/test_testing_harness.py::test_bind_test_uow_targets_the_named_database`<br>`sandloom/core/tests/test_testing_harness.py::test_required_audit_without_uow_raises` |
| C-TEST-07 | sandloom.sdk.testing.route\_policy(route) reports what the framework applies to a route: auth (user / service / public), the public reason (deny-by-default is assertable from an app suite), the effect (read / write by method) and the risks resolved from their sources; assert\_route\_snapshot(app, \*modules) fails unless each module's routes equal its committed routes.snapshot.json, naming the fix (SANDLOOM-T001), and fails on a missing snapshot. | `tests/sdk/test_sdk_testing_kit.py::test_route_policy_reads_auth_risk_and_the_public_reason`<br>`tests/sdk/test_sdk_testing_kit.py::test_assert_route_snapshot_pins_the_module_routes` |
| C-TEST-08 | published(E) lists the E events the code under test published with Event.publish(); a published() call without the testing plugin explains how to activate it; capture\_effects also records a typed Event.publish() made from the listed modules. | `tests/sdk/test_sdk_events.py::test_publish_writes_the_outbox_row_and_is_recorded`<br>`tests/sdk/test_sdk_events.py::test_a_view_cannot_publish`<br>`tests/sdk/test_sdk_events.py::test_capture_effects_records_typed_publishes` |
| C-TEST-09 | fake\_contract(REF \| name, impl) serves the contract to Contract(REF) for the block, nested, and restores it. | `tests/sdk/test_sdk_testing_kit.py::test_fake_contract_nests_and_restores` |
| C-TEST-10 | deliver drives the real consumer runtime (schema preflight, the durable claim, the tenant binding), accepts an Event instance and keeps today's deliver\_event parameters; as\_user is make\_user\_headers. | `tests/sdk/test_sdk_testing_kit.py::test_deliver_drives_the_real_consumer_once`<br>`tests/sdk/test_sdk_testing_kit.py::test_deliver_keeps_the_channel_form`<br>`tests/sdk/test_sdk_testing_kit.py::test_as_user_is_make_user_headers`<br>`examples/projecthub/projecthub/modules/notifications/tests/test_notifications.py::test_a_redelivered_event_notifies_once` |
| C-TEST-11 | Under the client fixture a route's session is the test's shared one however the route obtains it: a DbSession route and a db() route behave identically (writes visible to the test and to later requests, a failed request not rolled back, on\_commit callbacks after a successful request only); real\_uow\_client is the real-commit path for both. | `tests/integration/test_client_session_parity.py::test_a_dbsession_route_and_a_db_route_share_the_test_session` |
| C-TEST-12 | After every test the harness returns the pooled connections of the driver's default engines and of every database provider the active application built, so no later test (a new event loop) reuses a connection of a finished loop. | `sandloom/core/tests/test_release_pools.py::test_a_providers_own_engine_is_released_for_the_next_loop` |
| C-TEST-13 | sandloom test never aborts for want of parallel resources - -n auto is capped to the Redis database indexes the run holds or could claim (serially below two) with a warning, and the xdist steps run serially with a notice when pytest-xdist is not installed. | `sandloom/core/tests/test_testing_resources.py::test_auto_workers_fall_back_when_redis_indexes_are_short`<br>`sandloom/core/tests/test_testing_resources.py::test_affordable_workers_count_own_and_free_indexes_without_claiming`<br>`sandloom/core/tests/test_quickstart.py::test_sandloom_test_runs_serially_without_xdist` |
| C-TEST-14 | sandloom test runs a static schema &amp; deploy step (no database, no Redis) before the suites: migration-chain coverage read from the revisions' syntax tree (SANDLOOM-T002, a constant table name counts), redundant indexes (SANDLOOM-T012), one beat per module with periodic tasks in every deploy profile (SANDLOOM-T010), and a warning for a TenantScoped key without tenant\_id (SANDLOOM-T013). | `sandloom/core/tests/test_run_tests_cli.py::test_the_plan_of_the_framework_tree`<br>`sandloom/core/tests/test_model_indexes_migrated.py::test_tables_created_by_reads_the_chain_revisions`<br>`sandloom/core/tests/test_model_indexes_migrated.py::test_no_index_duplicates_a_unique_prefix`<br>`sandloom/core/tests/test_model_indexes_migrated.py::test_a_tenant_key_without_tenant_id_is_reported`<br>`sandloom/core/tests/test_deploy_profiles.py::test_a_module_with_periodic_tasks_and_no_beat_is_reported` |
| C-TEST-15 | fake\_contract(REF, impl) also serves the contract to kernel code that resolves it through the active application (core.get\_service - the billing and membership authz atoms), and fake\_access\_fact on the subscription facts drives the live SubscriptionActive / Subscribed(of=Account.org(...)) atoms, with or without billing loaded. | `sandloom/core/tests/test_testing_seams.py::test_fake_access_fact_drives_the_org_billing_atoms_without_billing`<br>`sandloom/core/tests/test_testing_seams.py::test_fake_contract_reaches_the_kernels_get_service` |

### C-WORKER

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-WORKER-01 | `celery -A sandloom.core.worker worker\\|beat` boots the composition root and imports .tasks (and setup\_periodic\_tasks) of every LOADED module only, skipping modules without tasks. | `sandloom/core/tests/test_worker.py::test_worker_registers_loaded_module_tasks_and_beat_schedule`<br>`sandloom/core/tests/test_worker.py::test_worker_without_identity_has_no_identity_tasks`<br>`sandloom/core/tests/test_worker.py::test_discover_skips_modules_without_tasks`<br>`sandloom/core/tests/test_worker.py::test_real_composition_root_resolves_driver_built_app` |
| C-WORKER-02 | The worker takes its Celery app from the taskqueue capability instance named by SANDLOOM\_WORKER\_TASKQUEUE, rejecting a non-Celery taskqueue and a composition root without a core; importing sandloom.core.worker has no side effects. | `sandloom/core/tests/test_worker.py::test_worker_honours_taskqueue_instance_env`<br>`sandloom/core/tests/test_worker.py::test_non_celery_taskqueue_is_rejected`<br>`sandloom/core/tests/test_worker.py::test_composition_root_must_expose_core`<br>`sandloom/core/tests/test_worker.py::test_import_has_no_side_effects` |
| C-WORKER-03 | Worker concurrency defaults from the CPU quota (capped, never the host CPU count) with WORKER\_CONCURRENCY overrides validated. | `sandloom/core/tests/test_worker.py::test_default_concurrency_follows_cpu_quota_and_is_capped`<br>`sandloom/core/tests/test_worker.py::test_sizing_never_falls_back_to_host_cpu_count`<br>`sandloom/core/tests/test_worker.py::test_sizing_env_overrides`<br>`sandloom/core/tests/test_worker.py::test_sizing_rejects_bad_values` |
| C-WORKER-04 | run\_async runs task coroutines on one persistent loop per worker process (fresh after fork, refused inside a running loop), draining the dispatcher and disposing engines on that loop at shutdown. | `sandloom/core/tests/test_async_runner.py::test_sequential_calls_share_one_persistent_loop`<br>`sandloom/core/tests/test_async_runner.py::test_inherited_loop_is_replaced_after_fork`<br>`sandloom/core/tests/test_async_runner.py::test_run_async_inside_a_running_loop_is_rejected`<br>`sandloom/core/tests/test_async_runner.py::test_dispatcher_is_drained_before_worker_returns_and_disposes_infra` |
| C-WORKER-05 | Celery tasks store no result by default and the worker prefetches one message (reliability defaults); enqueue never subscribes a result consumer. | `sandloom/core/tests/test_cache_batch_and_celery_defaults.py::test_worker_reliability_defaults`<br>`sandloom/core/tests/test_cache_batch_and_celery_defaults.py::test_driver_built_apps_ignore_results_by_default`<br>`sandloom/core/tests/test_cache_batch_and_celery_defaults.py::test_enqueue_never_subscribes_a_result_consumer` |
| C-WORKER-06 | The beat lease is scoped per service (replicas of one service share it, separate services do not) and wraps only this app's scheduler; the worker boot runs the tenant RLS role verification. | `sandloom/core/tests/test_beat_lease.py::test_lease_scope_separates_services_and_shares_replicas`<br>`sandloom/core/tests/test_beat_lease.py::test_beat_init_wraps_the_scheduler_of_this_app_only`<br>`sandloom/core/tests/test_beat_lease.py::test_two_leases_on_real_redis`<br>`sandloom/core/tests/test_worker.py::test_worker_boot_runs_tenant_rls_role_verification` |

### C-SAGA

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-SAGA-01 | Modules declare linear, versioned sagas (Saga\[InputT\], step, local\_step) with typed context and results, and start them with start\_saga / read them with saga\_status. | `sandloom/core/tests/test_sagas_definition.py::test_kinds_default_around_the_pivot`<br>`sandloom/core/tests/test_sagas_definition.py::test_context_key_and_typed_result`<br>`sandloom/core/tests/test_sagas_definition.py::test_a_matching_definition_builds`<br>`sandloom/core/tests/test_sagas_recovery.py::test_status_reads_the_handle` |
| C-SAGA-02 | Operators inspect and drive sagas with sandloom sagas list\|show\|retry\|park\|resolve (actionable default view, timeline with remediation) and sandloom doctor warns about parked instances; definitions appear in /diagnostics/core. | `sandloom/core/tests/test_sagas_cli.py::test_list_defaults_to_the_actionable_view`<br>`sandloom/core/tests/test_sagas_cli.py::test_show_prints_a_timeline_with_the_remediation`<br>`sandloom/core/tests/test_sagas_ops.py::test_list_and_show`<br>`sandloom/core/tests/test_sagas_channel.py::test_definitions_are_listed_in_core_diagnostics` |
| C-SAGA-03 | The saga\_harness test kit drives the real protocol with fault injection and one-line compensation/resolve checks. | `sandloom/core/tests/test_sagas_recovery.py::test_resolve_one_liner_on_the_kernel_saga`<br>`sandloom/core/tests/test_sagas_recovery.py::test_compensation_idempotence_one_liner` |

### C-DECL

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-DECL-01 | Event / on: typed construction, publish() through the outbox of the running unit (recorded for published()), meta before delivery raises EventMetaUnavailable. | `tests/sdk/test_sdk_events.py::test_construction_is_typed_and_strict`<br>`tests/sdk/test_sdk_events.py::test_meta_is_unavailable_before_delivery`<br>`tests/sdk/test_sdk_events.py::test_publish_writes_the_outbox_row_and_is_recorded`<br>`tests/sdk/test_sdk_events.py::test_abstract_base_shares_fields` |
| C-DECL-02 | contract / command / Contract: a stateless provider class becomes the L2 classmethod(contract\_method(...)) form with every option (idempotency\_key, idempotency\_scope, database, planned); **init**, instance attributes, non-async or public non-method members and bases outside the package are refused (SANDLOOM-B018); Contract(REF) resolves when called (fakes, then the application resolver); typed saga steps name the command; @command(risk="money") is passed to contract\_method(risk=) (the contract snapshot records it), and Contract(REF) resolves through the active core (sandloom.core.di.get\_service) outside the test plugin. | `tests/sdk/test_sdk_contracts.py::test_provider_becomes_the_l2_form_with_every_option`<br>`tests/sdk/test_sdk_contracts.py::test_stateful_or_non_async_providers_are_refused`<br>`tests/sdk/test_sdk_contracts.py::test_command_needs_its_key_parameter`<br>`tests/sdk/test_sdk_contracts.py::test_caller_resolves_when_called_through_a_fake`<br>`tests/sdk/test_sdk_contracts.py::test_caller_uses_the_bound_resolver`<br>`tests/sdk/test_sdk_contracts.py::test_contract_needs_a_generated_reference`<br>`tests/sdk/test_sdk_contracts.py::test_typed_saga_step_names_the_command`<br>`tests/sdk/test_sdk_discovery.py::test_provider_bases_must_be_inside_the_package`<br>`tests/sdk/test_sdk_contracts.py::test_the_default_resolver_is_the_kernels_active_core` |
| C-DECL-03 | module\_router: derived prefix and tags, no router-level auth; per route current\_context and TenantScope(required=...) unless public() or a service principal; a route that binds the tenant itself gets no second binder; public() with an authenticator is refused (SANDLOOM-B008); tenant="derive" is required iff the module declares a TenantScoped model; a route with a service authenticator stamped by sandloom.sdk.web.authenticates(kind="service") gets nothing injected. | `tests/sdk/test_sdk_router.py::test_prefix_and_tags_are_derived_from_the_module`<br>`tests/sdk/test_sdk_router.py::test_authentication_and_tenant_are_injected_per_route`<br>`tests/sdk/test_sdk_router.py::test_optional_binding_does_not_require_a_tenant`<br>`tests/sdk/test_sdk_router.py::test_public_and_service_routes_get_nothing`<br>`tests/sdk/test_sdk_router.py::test_public_with_an_authenticator_is_refused`<br>`tests/sdk/test_sdk_router.py::test_tenant_binding_is_derived_from_tenant_scoped_models`<br>`tests/sdk/test_sdk_router.py::test_unauthenticated_requests_are_refused_and_public_ones_served` |
| C-DECL-04 | Projection (relational targets): key starts with tenant\_id, one owning group per column, one version column per group, delta mappers only under the durable inbox, tombstones need their column (SANDLOOM-B012); full-state events converge under shuffles and duplicates, tombstones beat older upserts, deltas increment, views never return tombstones. | `tests/sdk/test_sdk_projections.py::test_invalid_projections_are_refused`<br>`tests/sdk/test_sdk_projections.py::test_the_l2_form_is_one_consumer_per_channel`<br>`tests/sdk/test_sdk_projections.py::test_full_state_converges_under_shuffles_and_duplicates`<br>`tests/sdk/test_sdk_projections.py::test_a_tombstone_beats_a_late_older_upsert`<br>`tests/sdk/test_sdk_projections.py::test_deltas_increment_and_versions_never_go_back`<br>`tests/sdk/test_sdk_projections.py::test_views_skip_tombstones_and_page`<br>`tests/sdk/test_sdk_projections.py::test_the_registered_handler_decodes_the_envelope`<br>`tests/laws/test_projection_laws.py::test_full_state_converges_to_the_max_version_event_per_group`<br>`tests/laws/test_projection_laws.py::test_a_tombstone_beats_any_late_older_upsert`<br>`tests/laws/test_projection_laws.py::test_a_column_has_exactly_one_owner_or_the_projection_is_refused`<br>`tests/laws/test_projection_laws.py::test_a_mapper_writing_a_column_its_group_does_not_own_is_refused`<br>`tests/laws/test_projection_laws.py::test_a_delta_mapper_is_accepted_only_under_the_durable_inbox`<br>`tests/laws/test_projection_laws.py::test_deltas_under_the_inbox_count_each_event_exactly_once` |
| C-DECL-05 | db() is the running unit's session wherever a unit runs: the request unit (lazy), open\_uow() blocks, consumers, tasks and saga steps, and inside a contract method the contract's session (contract\_session()), also under bind\_contract\_session. | `tests/integration/test_units_and_admission.py::test_db_in_a_block_is_that_units_session`<br>`sandloom/core/tests/test_db_in_contracts.py::test_db_in_a_bound_contract_is_the_bound_session` |

### C-SDK

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-SDK-01 | sandloom docs  answers a public name's home, tier, signature and documentation (and a module's names by tier) without a kernel read; every beginner code name defined in sandloom.sdk documents an example. | `tests/architecture/test_sdk_reference.py::test_sandloom_docs_answers_a_name`<br>`tests/architecture/test_sdk_reference.py::test_every_beginner_name_documents_an_example` |
| C-SDK-02 | Every public sandloom.sdk module starts with a generated reference block (beginner and advanced names, doc page) that is current (sandloom docs --write). | `tests/architecture/test_sdk_reference.py::test_every_public_module_carries_a_current_reference_block`<br>`tests/architecture/test_sdk_reference.py::test_a_block_lists_the_tiers_and_the_doc_page` |
| C-SDK-04 | sandloom docs describes what an app calls besides sandloom.sdk names, from the installed packages alone (the bundled sandloom.shared and the app's .shared): sandloom docs contracts / events list the generated stubs and exported schemas; sandloom docs  (name or REF constant) prints the import and each method's signature and the provider's docstring; sandloom docs  prints the fields, the producer's notes (versions, tenant field, ordering field) and a view to copy. Every method and class of a bundled contract stub states its semantics. | `tests/sdk/test_sdk_docs_wire.py::test_a_contract_prints_each_method_with_its_semantics`<br>`tests/sdk/test_sdk_docs_wire.py::test_an_event_prints_its_fields_and_the_producers_notes`<br>`tests/sdk/test_sdk_docs_wire.py::test_the_listings_name_every_stub_and_schema`<br>`tests/sdk/test_sdk_docs_wire.py::test_an_app_shared_package_is_read_too`<br>`tests/sdk/test_sdk_docs_wire.py::test_every_bundled_contract_method_states_its_semantics` |
| C-SDK-05 | The sandloom docs recipes with whole files (test-service-or-task, call-contract, projection-from-platform-events, event-v2, periodic-task) run as written in a generated app: their sandloom add / migrate new lines and \# file: blocks in text order, then sandloom generate and sandloom test on the whole app and sandloom docs from the installed packages (tools/smoke/recipes\_check.py, CI job app-recipes). | `tests/sdk/test_sdk_recipes.py::test_the_recipes_check_runs_every_verified_recipe`<br>`tests/sdk/test_sdk_recipes.py::test_recipe_parts_keep_the_text_order`<br>`tests/e2e/test_app_recipes.py::test_every_recipe_runs_in_a_generated_app` |
| C-SDK-06 | sandloom.sdk ships the id and timestamp idiom: new\_id() returns an RFC 9562 UUIDv7 (version 7, RFC 4122 variant, the current Unix millisecond) that strictly increases within a process (a burst in one millisecond, a clock stepping back, a full counter) and orders by time across processes; CreatedAt / UpdatedAt map timestamptz NOT NULL columns defaulting to clock\_timestamp() (UpdatedAt also on update), compose with TenantScoped, autogenerate as server\_default=sa.text('clock\_timestamp()'), and rows one transaction inserts list in creation order by created\_at then id. sandloom docs describes them and the list-order recipe; the bundled modules' chains migrate every such column to the clock\_timestamp() default. | `tests/sdk/test_sdk_columns.py::test_new_id_is_a_uuid7_stamped_with_the_current_millisecond`<br>`tests/sdk/test_sdk_columns.py::test_ids_of_a_burst_strictly_increase`<br>`tests/sdk/test_sdk_columns.py::test_the_fallback_orders_one_millisecond_and_a_clock_step_back`<br>`tests/sdk/test_sdk_columns.py::test_the_fallback_moves_to_the_next_millisecond_when_the_counter_is_full`<br>`tests/sdk/test_sdk_columns.py::test_ids_of_different_processes_order_by_time`<br>`tests/sdk/test_sdk_columns.py::test_the_timestamp_columns_default_to_clock_timestamp`<br>`tests/sdk/test_sdk_columns.py::test_the_columns_compose_with_tenant_scoped`<br>`tests/sdk/test_sdk_columns.py::test_alembic_autogenerate_renders_clock_timestamp`<br>`tests/sdk/test_sdk_columns.py::test_rows_of_one_transaction_list_in_creation_order`<br>`tests/sdk/test_sdk_columns.py::test_sandloom_docs_describes_the_idiom`<br>`tests/integration/test_migrations_per_chain.py::test_chains_reproduce_orm_schema_exactly` |
| C-SDK-03 | sandloom.sdk.tasks.shared\_task is Celery's decorator typed with the task's signature (call, delay, apply\_async, s), so a task module type-checks under strict mypy with no project override; cron() builds a beat schedule from a five-field expression. | `tests/sdk/test_sdk_generators.py::test_a_fully_generated_app_passes_the_static_gates` |

### C-GEN

| Id | Statement | Enforced by |
| --- | --- | --- |
| C-GEN-01 | sandloom add module writes new files inside the package only (model, schemas, services, events, module\_router routes, a baseline migration with RLS rendered from the model, tests) and refuses an existing package or a bad name. | `tests/sdk/test_sdk_generators.py::test_add_module_writes_only_inside_the_package` |
| C-GEN-02 | sandloom add event appends a producer class to events.py, never rewriting it, and refuses a file that does not parse or lacks an import. | `tests/sdk/test_sdk_generators.py::test_add_event_appends_and_refuses_an_unparsable_file` |
| C-GEN-03 | sandloom add contract writes the provider in contracts.py and its DTO in the module's own schemas (the provider authors its DTOs; a second contract of the module joins the same schemas import), and writes the stub and the DTO copy at once exactly as sandloom generate renders them (so a saga or caller in the same scaffold boots); a generated stub imports ContractRef from its kernel home sandloom.core.descriptors.contract\_refs (no sandloom.sdk import, no advanced marker), names the generated DTO copies, passes its Protocol, and is ruff format clean. | `tests/sdk/test_sdk_generators.py::test_add_contract_writes_the_generated_stub`<br>`tests/sdk/test_sdk_generated.py::test_a_generated_stub_is_ruff_formatted` |
| C-GEN-04 | sandloom add consumer writes a view from the producer schema (the tree or the exported file); without one it needs --field and marks the view unverified. | `tests/sdk/test_sdk_generators.py::test_add_consumer_takes_the_producer_schema_or_marks_unverified` |
| C-GEN-05 | sandloom add saga writes package files only; an existing target is kept and the output written as \*.new. | `tests/sdk/test_sdk_generators.py::test_an_existing_target_is_never_overwritten` |
| C-GEN-06 | sandloom add projection writes the projection, its row model and a migration chained after the module chain head; a fully generated app (every generator) passes ruff, format, mypy --strict and sandloom check with the pending 1.0 checks, and every one of its tests passes. | `tests/sdk/test_sdk_generators.py::test_add_projection_chains_its_migration_after_the_head`<br>`tests/sdk/test_sdk_generators.py::test_a_fully_generated_app_passes_the_static_gates`<br>`tests/sdk/test_sdk_generators.py::test_a_fully_generated_app_passes_its_tests_up_to_the_h1_stubs` |
| C-GEN-07 | sandloom migrate new adds enable\_tenant\_rls / disable\_tenant\_rls (system\_access for **system\_access** models) for every TenantScoped table the autogenerated revision creates. | `tests/sdk/test_sdk_generators.py::test_migrate_new_adds_row_level_security_to_new_tenant_tables` |
| C-GEN-08 | sandloom add module wires the module where its project loads modules (the framework tree's LOCAL\_MODULES, SANDLOOM\_MODULES lists, the monolith profile; an sandloom new app discovers it) and, in the framework tree, gives its tests the kit's fixtures (client, published recording) through a one-line conftest. | `tests/sdk/test_sdk_generators.py::test_add_module_in_the_framework_tree_is_wired_and_gets_the_kit` |
| C-GEN-09 | sandloom add task   \[--periodic ""\] writes an idempotent at-least-once task (acks\_late, reject\_on\_worker\_lost, run\_async with an open\_uow block) that passes the app's static gates, registers a periodic one in the module's setup\_periodic\_tasks, and wires the beat or prints the step. | `tests/sdk/test_sdk_generators.py::test_add_task_writes_an_idempotent_task_and_its_schedule`<br>`tests/sdk/test_sdk_generators.py::test_a_fully_generated_app_passes_the_static_gates` |
| C-GEN-10 | The quickstart sandloom new prints is the documented one (one source) and passes as printed in an empty directory - uv sync, sandloom add module, sandloom test, migrations, the app serving /health and an authenticated round trip, sandloom test --ci - with the dev services' ports configurable. | `sandloom/core/tests/test_quickstart.py::test_the_docs_quickstart_is_what_sandloom_new_prints`<br>`sandloom/core/tests/test_quickstart.py::test_dev_service_ports_are_configurable`<br>`sandloom/core/tests/test_quickstart.py::test_sandloom_new_writes_the_generated_files`<br>`sandloom/core/tests/test_quickstart.py::test_quickstart_end_to_end` |
| C-GEN-11 | sandloom add module generates a list route (GET /items) whose limit is capped with Query(le=...) over its TenantScoped table, newest first in a deterministic order (the model uses the framework's idiom - id defaults to new\_id, a UUIDv7, and created\_at is Mapped\[CreatedAt\], which defaults to clock\_timestamp(), so rows created in one transaction do not tie - and the list orders by created\_at then id), and a test that covers the order of rows created in one transaction, the page size, the cap (422) and another tenant's empty list. | `tests/sdk/test_sdk_generators.py::test_add_module_lists_items_capped_and_tenant_scoped`<br>`tests/sdk/test_sdk_generators.py::test_add_module_lists_newest_first_deterministically_in_one_transaction`<br>`tests/sdk/test_sdk_generators.py::test_a_fully_generated_app_passes_its_tests_up_to_the_h1_stubs`<br>`tests/sdk/test_sdk_columns.py::test_rows_of_one_transaction_list_in_creation_order` |
| C-GEN-12 | sandloom new --framework-source path writes editable sources, and an editable install copies no sandloom file (its .pth puts the live src/sandloom on sys.path), so the app always runs the checkout's current tree, never a cached build of the same version; the quickstart job fails when the installed framework is not the checkout. | `sandloom/core/tests/test_quickstart.py::test_framework_path_takes_the_repository_root`<br>`sandloom/core/tests/test_quickstart.py::test_an_editable_framework_install_is_the_live_tree`<br>`sandloom/core/tests/test_quickstart.py::test_quickstart_end_to_end` |
| C-GEN-13 | sandloom new writes the app's agent skills (sandloom-new-module, sandloom-quality-gates, sandloom-event-version-roll, sandloom-split-deploy), rewritten for the app's paths and commands, to .claude/skills and .agents/skills, and the app map names them in one line; sandloom add skills writes them into an existing app, keeping an edited file unless --force. | `sandloom/core/tests/test_project.py::test_generated_project_ships_the_app_skills`<br>`sandloom/core/tests/test_project.py::test_sandloom_add_skills_refreshes_an_existing_app` |
| C-GEN-14 | Example apps pass their own gate - every example app (examples/*) and the tutorial app (docs/tutorial-app/*) passes the gate an app developer runs, sandloom test --ci --full (generated files checked, schema &amp; deploy, the full suite, ruff check without fixing, ruff format, mypy, sandloom check, contract and route snapshots, migration drift), in the CI examples job, which lists every such app; a generated contract stub is exactly what ruff format writes. | `tests/architecture/test_ci_local_runner.py::test_every_example_and_the_tutorial_app_run_their_own_gate_in_ci`<br>`sandloom/core/tests/test_run_tests_cli.py::test_ci_never_writes_so_ruff_check_never_fixes`<br>`tests/sdk/test_sdk_generated.py::test_a_rendered_stub_is_what_ruff_format_writes`<br>`tests/sdk/test_sdk_generated.py::test_a_generated_stub_is_ruff_formatted` |
| C-GEN-15 | sandloom generate \[module ...\] writes every generated file - module, route and contract snapshots, contract stubs, generated DTO copies, exported event schemas - after the compatibility checks (an incompatible change fails with its code and nothing is written; a weaker route policy is refused, pointing to sandloom routes snapshot --write); module names narrow it to those modules' files. sandloom test never writes a generated file, dev and --ci alike: a stale or hand-edited one fails, listed under "generated files are stale: run `sandloom generate`". Every sandloom add generator (but skills and deploy) runs sandloom generate last, and sandloom new writes them through it, so the files are current after scaffolding. | `sandloom/core/tests/test_run_tests_cli.py::test_sandloom_test_never_writes_a_generated_file`<br>`tests/sdk/test_sdk_generated.py::test_a_stale_file_asks_for_sandloom_generate_and_is_left_alone`<br>`tests/sdk/test_sdk_generated.py::test_generate_refuses_a_weakened_route_and_names_the_approval`<br>`tests/sdk/test_sdk_generated.py::test_an_incompatible_change_fails_before_anything_is_written`<br>`tests/sdk/test_sdk_generators.py::test_sandloom_add_runs_generate_at_the_end`<br>`tests/sdk/test_sdk_generators.py::test_add_contract_writes_the_generated_stub`<br>`sandloom/core/tests/test_quickstart.py::test_sandloom_new_writes_the_generated_files` |

## Gaps

| Id | Size | Missing |
| --- | --- | --- |
| C-EVENT-01 | S | No test proves that publish\_event\_versions inside a UoW writes all versions as outbox rows atomically (rolled back together). |
| C-INFRA-05 | S | no test resolves a Capability(...) dependency in a request and asserts it yields the named provider (or the contract/feature error) |
| C-INFRA-10 | S | no test calls the /diagnostics/infra endpoint itself (only registry.describe(); the route appears only in the operation-class map) |
| C-MIGRATE-03 | S | Only flag plumbing is tested; no test runs upgrade-core --database  against a second database. |
| C-OBS-06 | S | No test asserts Sentry init is a no-op without SENTRY\_DSN / initialises with it. |
| C-OPS-02 | S | The `sandloom outbox purge` CLI entry point itself is not invoked by a test. |
| C-PROJ-03 | S | \--tenant scoping of a rebuild is not exercised by a test. |
| C-PROJ-04 | S | No test (framework or examples) uses insert\_only; a unit test proving INSERT seeds and UPDATE preserves the column is missing. |
| C-TENANT-02 | S | TenantScope(required=False) yielding None and leaving the context unbound, and custom header/claim\_keys, are untested. |
| G-AUDIT-01 | S | No generic test that a plain record\_audit followed by a UoW rollback leaves no audit\_log row; only the block\_user path proves it. |
| G-AUDIT-02 | S | The 'no audit store' branch of AuditUnavailableError (database driver without the audit feature) has no direct raising test (test\_audit\_store\_skips\_unsupported\_backend only checks the store is absent). |
| G-COMPAT-03 | S | No test drives `sandloom contracts check --against http://<provider>` (live describe document over the internal token) or the `sandloom contracts snapshot --write/--check` CLI exit codes (only the underlying functions are tested). |
| G-COMPAT-07 | S | The startup log for a remote binding with no snapshot at all ("unchecked", CONTRACT\_SNAPSHOT\_PATHS absent) is not asserted. |
| G-CONFIG-05 | S | sandloom\_config\_override\_active{setting,kind} metric export (app\_factory) is not asserted by any test. |
| G-CONFIG-08 | S | AGENTS.md and accepted\_risks error messages point at docs/reference/configuration-tiers.md, but that file does not exist in the repo (only tiers.render\_markdown() in memory); nothing checks the committed page exists / is current. |
| G-EFFECT-03 | S | No test proves that a before\_commit check registered inside a rolled-back savepoint is skipped (ADR §3 stage 1). |
| G-EVENT-05 | S | No test asserts the handler's DB writes are rolled back when the handler raises (the consumer UoW rollback itself); tests check redelivery with fake sessions only. |
| G-GUARD-01 | S | Tests call the order checker directly; no test boots create\_app with a misordered route to prove the startup wiring. |
| G-INFRA-06 | S | no test asserts the preflight performs no I/O (opens no provider/connection), nor that a failed required preflight aborts create\_app for a required module |
| G-MIGRATE-05 | S | Doc drift: AGENTS.md lists four core tables (outbox/audit/consumer\_inbox/contract\_command\_log) but the chain (and the test) also own http\_idempotency\_log. |
| G-MIGRATE-06 | S | The prod refusal of ensure-outbox --apply without --force (cli/migrate.py) is untested. |
| G-MODULE-03 | S | no test proves a module importing sandloom.core.module\_api.\_service\_registry or sandloom.core.facade.\_state is flagged (only that the tree is clean) |
| G-OPS-12 | S | The prod --confirm-production requirement of sandloom security is untested (only sandloom backup's guard rails test it). |
| G-OUTBOX-04 | S | No test calls publish\_event INSIDE a UoW with an invalid channel / schema-violating / oversized payload and asserts the call raises and no outbox\_messages row exists; the in-UoW path (unit\_of\_work/effects.py preflight) is only covered indirectly. |
| G-PROFILE-05 | S | enforce\_runtime is tested directly, but no test proves it is wired as a critical startup task of create\_app's lifespan or of the Celery worker boot (removing either call site would stay green). |
| G-PROFILE-06 | S | The 'Cannot attest separate ... Redis processes' failure (deployment\_identity raising) has no test. |
| G-REVOKE-06 | S | No test pins that password change/reset and user deactivation use the immediate (pre-commit) path specifically, rather than only that tokens end up revoked. |
| G-RISK-02 | S | Export of sandloom\_accepted\_risk\_active{risk}=1 (app\_factory) is never asserted; a test reading the metric after create\_app in prod with a risk accepted is missing. |
| G-RISK-11 | S | No test exercises the unversioned\_ddl\_repair gate in sandloom/core/cli/migrate.py (only ensure-outbox dry-run / dev apply are tested). |
| G-RLS-05 | S | The BYPASSRLS-attribute branch (and membership in a BYPASSRLS/superuser role) has no direct test; only SUPERUSER, table ownership and system-role membership are exercised. |
| G-RLS-08 | S | 'No audit row, no commit' (a failing audit write aborts the system session's commit) and the structured start/end log record (op\_id, outcome, duration) are not tested. |
| G-TENANT-14 | S | The 'token tenant does not match the configured tenant' 403 (token claim, no header) branch has no test. |
| G-TENANT-17 | S | The rule is only tested at the analyzer level (find\_unsafe\_tenant\_sql); no test runs it through run\_checks/sandloom check on a module fixture as the tenant-scoped-models rule is. |
| G-UOW-06 | S | Only the private resolver is unit-tested; no end-to-end test holds DbSession + Database("x") in one endpoint and asserts the event lands in the chosen database's outbox\_messages (and is marked delivered there). |
| G-UOW-07 | S | Non-atomicity itself is a documented non-guarantee; no test pins that a failing second-database UoW leaves the first committed (i.e. that nobody quietly introduces a coupled commit). |
| G-BLOCK-04 | M | No generic check forces a custom (app-defined) issuer to call ensure\_user\_not\_blocked; the rule is documentation-only for third-party issuers. |
| G-MODULE-17 | M | convention only: no sandloom check rule or test rejects a ForeignKey from one module's table to another module's table prefix |

## Removed

| Id | Statement | Reason | Approved | Date |
| --- | --- | --- | --- | --- |
| C-TEST-04 | sandloom.core.testing.assert\_operation\_classes(app) fails an app with an unclassified or contradictorily classified route; sandloom new generates the check. | superseded by C-TEST-07 (ADR-0009, accepted by the owner; spec §9.2): ADR-0009 removes assert\_operation\_classes outright; apps assert deny-by-default with sandloom.sdk.testing.route\_policy(route) and pin their routes with assert\_route\_snapshot (C-TEST-07). | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-GUARD-03 | create\_app refuses (FinancialGuardError) an operation\_class('F') route with a response-storing cache guard or a fail-open guard. | superseded by G-SENS-05 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-IDEM-13 | create\_app refuses an operation\_class("F") route whose idempotency guard is the response-storing cache form or fail-open (FinancialGuardError); durable=True or store\_response=False + fail\_closed=True are accepted. | superseded by G-SENS-05 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-OPCLASS-01 | Every API route of the composed monolith (kernel ops routes + identity + tenancy + billing) declares exactly one ADR-0003 operation class (P/R/RS/W/F/A) on the route or its router; an unclassified route fails CI, and apps run the same gate via sandloom.core.testing.assert\_operation\_classes(app). | superseded by G-ROUTE-01 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-OPCLASS-02 | create\_app refuses (OperationClassError) a route with two different classes, an unknown class, a revocation\_freshness budget outside class R, or a class RS/W/F/A route that is not always-strict. | superseded by G-ROUTE-01 / G-ROUTE-04 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-REVOKE-13 | revocation\_freshness(max\_staleness\_s=N) requires 0 \< N \<= 5 (else ValueError) and may be declared only on GET/HEAD routes: create\_app refuses a budget on any other method or on a router with a write route (RevocationPolicyError). | superseded by G-REVOKE-21 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-REVOKE-14 | A budgeted route answers from a per-process mark copy only while it is younger than the budget at DECISION time (re-checked after the live block read); a copy is never extended, and a copy expired or invalidated while a read was in flight is replaced by a live read of the mark. | superseded by G-REVOKE-21 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-REVOKE-15 | revoke\_user\_tokens drops the local copy and broadcasts on Redis pub/sub (sandloom:auth:revocations); other processes drop theirs, a lost broadcast delays a revocation on budgeted routes by at most the budget, only a drop of the same subject (or a wholesale clear on (re)subscribe) discards an in-flight read. | superseded by G-REVOKE-21 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-REVOKE-16 | A route marked always-strict (Depends(strict\_revocation()) on it or its router, or a path under AUTH\_REVOCATION\_STRICT\_PATH\_PREFIXES) ignores a revocation\_freshness budget (logged at startup) and always reads the mark live. | superseded by G-REVOKE-21 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |
| G-REVOKE-20 | Per-request revocation cost: a strict route does one Redis round trip (MGET of revocation + block marks); a budgeted route with a fresh local copy does one GET of the block mark. | superseded by G-REVOKE-21 (ADR-0009, accepted by the owner; spec §9.2) | owner (ADR-0009 / ADR-0010 approval, 2026-10-25) | 2026-10-26 |

## Sources

| Id | Source |
| --- | --- |
| G-TENANT-01 | docs/modules/database/multi-tenancy.md<br>docs/reference/security-model.md\#Tenant isolation |
| G-TENANT-02 | docs/modules/database/multi-tenancy.md<br>docs/reference/security-model.md\#Tenant isolation |
| G-TENANT-03 | docs/modules/database/multi-tenancy.md<br>docs/reference/security-model.md\#Tenant isolation |
| G-TENANT-04 | docs/modules/database/multi-tenancy.md |
| G-TENANT-05 | docs/modules/database/multi-tenancy.md |
| G-TENANT-06 | docs/modules/database/multi-tenancy.md |
| G-TENANT-07 | docs/modules/database/multi-tenancy.md<br>docs/reference/security-model.md\#Tenant isolation |
| G-TENANT-08 | docs/modules/database/multi-tenancy.md |
| G-TENANT-09 | docs/modules/database/multi-tenancy.md |
| G-TENANT-10 | docs/modules/database/multi-tenancy.md |
| G-TENANT-11 | docs/modules/database/multi-tenancy.md |
| G-TENANT-12 | docs/modules/database/multi-tenancy.md<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.2 Route kinds |
| G-TENANT-13 | docs/modules/database/multi-tenancy.md |
| G-TENANT-14 | docs/modules/database/multi-tenancy.md |
| G-TENANT-15 | docs/modules/database/multi-tenancy.md |
| G-TENANT-16 | docs/modules/database/multi-tenancy.md |
| G-TENANT-17 | docs/modules/database/multi-tenancy.md |
| G-TENANT-18 | docs/modules/between/projections.md |
| G-RLS-01 | docs/reference/security-model.md\#What is guaranteed (tables with `enable_tenant_rls`, startup verification passed)<br>docs/reference/security-model.md\#SQL paths available to business-module code |
| G-RLS-02 | docs/reference/security-model.md\#What is guaranteed (tables with `enable_tenant_rls`, startup verification passed) |
| G-RLS-03 | docs/reference/security-model.md\#What is guaranteed (tables with `enable_tenant_rls`, startup verification passed)<br>docs/modules/database/multi-tenancy.md |
| G-RLS-04 | docs/reference/security-model.md\#What is guaranteed (tables with `enable_tenant_rls`, startup verification passed) |
| G-RLS-05 | docs/reference/security-model.md\#What is guaranteed (tables with `enable_tenant_rls`, startup verification passed)<br>docs/modules/database/multi-tenancy.md |
| G-RLS-06 | docs/modules/database/multi-tenancy.md |
| G-RLS-07 | docs/modules/database/multi-tenancy.md |
| G-RLS-08 | docs/reference/security-model.md\#The system channel (privileged work)<br>docs/modules/database/multi-tenancy.md |
| G-RLS-09 | docs/reference/security-model.md\#The system channel (privileged work) |
| G-RLS-10 | docs/reference/security-model.md\#SQL paths available to business-module code<br>docs/modules/database/multi-tenancy.md |
| G-RLS-11 | docs/reference/security-model.md\#SQL paths available to business-module code<br>docs/reference/security-model.md\#What is NOT guaranteed (residual risks) |
| G-RLS-12 | docs/reference/security-model.md\#SQL paths available to business-module code |
| G-RLS-13 | docs/reference/security-model.md\#What is guaranteed (tables with `enable_tenant_rls`, startup verification passed) |
| G-RLS-14 | docs/reference/security-model.md\#What is NOT guaranteed (residual risks)<br>docs/reference/security-model.md\#Trust model of this Sandloom version (normative) |
| G-RLS-15 | docs/reference/security-model.md\#Tenant RLS threat model |
| G-PROFILE-01 | docs/reference/security-model.md\#`PRODUCTION_PROFILE=multi_tenant_strict`<br>docs/modules/database/multi-tenancy.md |
| G-PROFILE-02 | docs/reference/security-model.md\#`PRODUCTION_PROFILE=multi_tenant_strict` |
| G-PROFILE-03 | docs/reference/security-model.md\#`PRODUCTION_PROFILE=multi_tenant_strict`<br>docs/reference/security-model.md\#The contract (what Sandloom guarantees, and under which conditions) |
| G-PROFILE-04 | docs/reference/security-model.md\#Production profile: the multi-tenant trust model as a contract |
| G-PROFILE-05 | docs/reference/security-model.md\#`PRODUCTION_PROFILE=multi_tenant_strict` |
| G-PROFILE-06 | docs/reference/security-model.md\#`PRODUCTION_PROFILE=multi_tenant_strict`<br>docs/modules/database/multi-tenancy.md |
| G-PROFILE-07 | docs/reference/security-model.md\#`PRODUCTION_PROFILE=multi_tenant_strict`<br>docs/reference/security-model.md\#Security-critical vs best-effort cache |
| G-PROFILE-08 | docs/reference/security-model.md\#Security-critical vs best-effort cache<br>docs/modules/database/multi-tenancy.md |
| G-PROFILE-09 | docs/reference/security-model.md\#`PRODUCTION_PROFILE=multi_tenant_strict` |
| G-UOW-01 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list |
| G-UOW-02 | docs/modules/database/units.md |
| G-UOW-03 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list |
| G-UOW-04 | docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list |
| G-UOW-05 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list |
| G-UOW-06 | docs/modules/database/units.md<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-UOW-07 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#1. Vocabulary |
| G-UOW-08 | docs/modules/database/units.md |
| G-UNIT-01 | docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.1 Units<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.3 `db()` ownership |
| G-UNIT-02 | docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.6 Operation-intent mode (billing checkout and portal) |
| G-UNIT-03 | docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.4 Write admission: one writer per request |
| G-MSTORE-01 | docs/adr/0010-public-api-1.0.md\#5. One writer per request<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.1 Write store |
| G-MSTORE-02 | docs/adr/0010-public-api-1.0.md\#5. One writer per request<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.1 Write store |
| G-MSTORE-03 | docs/adr/0010-public-api-1.0.md\#5. One writer per request<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.4 Write admission: one writer per request |
| G-MSTORE-04 | docs/adr/0010-public-api-1.0.md\#5. One writer per request<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.4 Write admission: one writer per request |
| G-MSTORE-05 | docs/adr/0010-public-api-1.0.md\#5. One writer per request<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.1 Write store |
| G-MSTORE-06 | docs/adr/0010-public-api-1.0.md\#5. One writer per request<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.1 Write store |
| G-MSTORE-07 | docs/adr/0010-public-api-1.0.md\#6. Guarantees as a lattice, labels derived from mechanisms (amends ADR-0005)<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.4 Projection convergence model |
| G-MSTORE-08 | docs/adr/0010-public-api-1.0.md\#6. Guarantees as a lattice, labels derived from mechanisms (amends ADR-0005)<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.4 Projection convergence model |
| G-MSTORE-09 | docs/adr/0010-public-api-1.0.md\#6. Guarantees as a lattice, labels derived from mechanisms (amends ADR-0005)<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.4 Projection convergence model |
| G-MSTORE-10 | docs/adr/0010-public-api-1.0.md\#6. Guarantees as a lattice, labels derived from mechanisms (amends ADR-0005)<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.4 Projection convergence model |
| G-OUTBOX-01 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-02 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-03 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-04 | docs/infrastructure/index.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-05 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-06 | docs/modules/database/units.md |
| G-OUTBOX-07 | docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-08 | docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list |
| G-OUTBOX-09 | docs/modules/index.md\#What you get by default, and what you declare |
| G-OUTBOX-10 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix<br>docs/operate/operator-reference.md\#Events: outbox relay and dead letters |
| G-OUTBOX-11 | docs/reference/cli.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-12 | docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-13 | docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-OUTBOX-14 | docs/reference/cli.md |
| G-OUTBOX-15 | docs/adr/0005-effect-model.md\#1. Vocabulary<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-OUTBOX-16 | docs/adr/0005-effect-model.md\#6a. Declared requirements and guarantee windows (startup checks)<br>docs/reference/cli.md |
| G-EFFECT-01 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-EFFECT-02 | docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list |
| G-EFFECT-03 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list |
| G-EFFECT-04 | docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive<br>docs/adr/0005-effect-model.md\#6. Rule for new mechanisms (complexity throttle) |
| G-EFFECT-05 | docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive |
| G-INBOX-01 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive |
| G-INBOX-02 | docs/modules/index.md\#What a module may import<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-INBOX-03 | docs/adr/0005-effect-model.md\#4. Mechanism matrix<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-INBOX-04 | docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive |
| G-CMD-01 | docs/modules/between/contracts.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix<br>docs/adr/0005-effect-model.md\#4a. Ambiguous COMMIT |
| G-CMD-02 | docs/modules/between/contracts.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-CMD-03 | docs/modules/between/contracts.md |
| G-CMD-04 | docs/modules/between/contracts.md |
| G-CMD-05 | docs/modules/between/contracts.md<br>docs/adr/0005-effect-model.md\#Context |
| G-CMD-06 | docs/modules/between/contracts.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-CMD-07 | docs/modules/between/contracts.md<br>docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive |
| G-CMD-08 | docs/modules/between/contracts.md |
| G-CMD-09 | docs/modules/between/contracts.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-IDEM-01 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Idempotency |
| G-IDEM-02 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-IDEM-03 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Idempotency |
| G-IDEM-04 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive |
| G-IDEM-05 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Idempotency |
| G-IDEM-06 | docs/reference/security-model.md\#Idempotency<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-IDEM-07 | docs/modules/index.md\#What you get by default, and what you declare |
| G-IDEM-08 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Idempotency |
| G-IDEM-09 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-IDEM-10 | docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive |
| G-IDEM-11 | docs/adr/0005-effect-model.md\#4a. Ambiguous COMMIT |
| G-IDEM-12 | docs/adr/0005-effect-model.md\#2. The dedupe record — one primitive<br>docs/adr/0005-effect-model.md\#Consequences |
| G-IDEM-14 | docs/modules/index.md\#What you get by default, and what you declare |
| G-IDEM-15 | docs/reference/security-model.md\#Idempotency |
| G-IDEM-16 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/modules/database/multi-tenancy.md |
| G-IDEM-17 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare)<br>docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle |
| G-IDEM-18 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare)<br>docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle |
| G-TASK-01 | docs/modules/background/tasks.md<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-TASK-02 | docs/modules/background/tasks.md |
| G-TASK-03 | docs/modules/background/tasks.md<br>docs/operate/operator-reference.md\#Uncertain task enqueues<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-TASK-04 | docs/modules/background/tasks.md<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-TASK-05 | docs/modules/index.md\#What you get by default, and what you declare |
| G-WINDOW-01 | docs/adr/0005-effect-model.md\#6a. Declared requirements and guarantee windows (startup checks) |
| G-WINDOW-02 | docs/adr/0005-effect-model.md\#6a. Declared requirements and guarantee windows (startup checks) |
| G-WINDOW-03 | docs/adr/0005-effect-model.md\#6a. Declared requirements and guarantee windows (startup checks)<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-EVENT-01 | docs/modules/index.md\#What a module may import |
| G-EVENT-02 | docs/modules/index.md\#What a module may import |
| G-EVENT-03 | docs/modules/index.md\#What a module may import |
| G-EVENT-04 | docs/modules/index.md\#What a module may import |
| G-EVENT-05 | docs/modules/index.md\#What a module may import |
| G-EVENT-06 | docs/modules/index.md\#What a module may import<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-EVENT-07 | docs/modules/index.md\#What a module may import |
| G-EVENT-08 | docs/modules/index.md\#What a module may import |
| G-EVENT-09 | docs/modules/index.md\#What a module may import |
| G-EVENT-10 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/operate/operator-reference.md\#Events: outbox relay and dead letters |
| G-EVENT-11 | docs/modules/index.md\#What you get by default, and what you declare |
| G-EVENT-12 | docs/modules/index.md\#What you get by default, and what you declare |
| G-EVENT-13 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/modules/between/events.md\#Changing an event |
| G-EVENT-14 | docs/reference/cli.md |
| G-AUDIT-01 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Audit trail<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-AUDIT-02 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0005-effect-model.md\#4. Mechanism matrix |
| G-AUTH-01 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-AUTH-02 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-AUTH-03 | docs/reference/security-model.md\#Authentication<br>docs/reference/cli.md |
| G-AUTH-04 | docs/reference/security-model.md\#Authentication |
| G-AUTH-05 | docs/reference/security-model.md\#Authentication<br>docs/reference/cli.md |
| G-AUTH-06 | docs/reference/security-model.md\#Authentication<br>docs/operate/topologies.md |
| G-AUTH-07 | docs/reference/security-model.md\#Authentication |
| G-AUTH-08 | docs/reference/security-model.md\#Authentication |
| G-AUTH-09 | docs/reference/security-model.md\#Authentication |
| G-AUTH-10 | docs/reference/security-model.md\#Secrets<br>docs/reference/cli.md |
| G-AUTH-11 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authorization |
| G-AUTH-12 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authorization |
| G-AUTH-13 | docs/reference/security-model.md\#Authorization |
| G-AUTH-14 | docs/modules/index.md\#What you get by default, and what you declare |
| G-AUTH-15 | docs/reference/security-model.md\#Authentication<br>docs/reference/cli.md |
| G-AUTH-16 | docs/reference/security-model.md\#Authentication<br>docs/reference/cli.md |
| G-AUTH-17 | docs/reference/security-model.md\#Authentication<br>docs/reference/cli.md |
| G-AUTH-18 | docs/modules/standard/identity.md\#Behaviour API clients must know |
| G-AUTH-19 | docs/reference/security-model.md\#Authentication<br>docs/reference/cli.md<br>docs/operate/topologies.md |
| G-REVOKE-01 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-REVOKE-02 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-REVOKE-03 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-REVOKE-04 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-REVOKE-05 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-REVOKE-06 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-REVOKE-07 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-REVOKE-08 | docs/reference/security-model.md\#Authentication |
| G-REVOKE-09 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-REVOKE-10 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication<br>docs/adr/0009-route-policy-and-session-freshness.md\#One subject-state record replaces the two marks |
| G-REVOKE-11 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Failure rules (fail-closed defaults)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-REVOKE-12 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Decisions so far<br>docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-REVOKE-17 | docs/modules/database/units.md<br>docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Guarantees, precisely<br>docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-REVOKE-18 | docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-REVOKE-19 | docs/reference/security-model.md\#Security-critical vs best-effort cache<br>docs/reference/cli.md |
| G-REVOKE-21 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare)<br>docs/adr/0009-route-policy-and-session-freshness.md\#One subject-state record replaces the two marks<br>docs/adr/0009-route-policy-and-session-freshness.md\#Checks (deterministic; each error names its fix) |
| G-REVOKE-22 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Checks (deterministic; each error names its fix)<br>docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle |
| G-REVOKE-23 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-REVOKE-24 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-BLOCK-01 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication<br>docs/adr/0009-route-policy-and-session-freshness.md\#One subject-state record replaces the two marks<br>docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-BLOCK-02 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication<br>docs/adr/0009-route-policy-and-session-freshness.md\#One subject-state record replaces the two marks |
| G-BLOCK-03 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication<br>docs/adr/0009-route-policy-and-session-freshness.md\#One subject-state record replaces the two marks |
| G-BLOCK-04 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Operation classes (normative) |
| G-BLOCK-05 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| G-BLOCK-06 | docs/adr/0009-route-policy-and-session-freshness.md\#One subject-state record replaces the two marks |
| G-BLOCK-07 | docs/adr/0009-route-policy-and-session-freshness.md\#One subject-state record replaces the two marks |
| G-OPCLASS-03 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Platform routes → classes<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Decisions so far<br>docs/adr/0009-route-policy-and-session-freshness.md\#The route snapshot |
| G-OPCLASS-04 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication<br>docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-OPCLASS-05 | docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Gaps the classification exposed — resolved<br>docs/adr/0009-route-policy-and-session-freshness.md\#Risk is declared at its source, once, by its owner |
| G-OPCLASS-06 | docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Gaps the classification exposed — resolved<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Guarantees, precisely<br>docs/adr/0009-route-policy-and-session-freshness.md\#Risk is declared at its source, once, by its owner |
| G-OPCLASS-07 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Platform owner console routes<br>docs/adr/0009-route-policy-and-session-freshness.md\#Risk is declared at its source, once, by its owner |
| G-ROUTE-01 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Checks (deterministic; each error names its fix)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Removed from the internal API before the initial public alpha |
| G-ROUTE-02 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-ROUTE-03 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-ROUTE-04 | docs/adr/0009-route-policy-and-session-freshness.md\#The route snapshot<br>docs/adr/0009-route-policy-and-session-freshness.md\#Checks (deterministic; each error names its fix) |
| G-ROUTE-05 | docs/adr/0009-route-policy-and-session-freshness.md\#The route snapshot |
| G-SENS-01 | docs/adr/0009-route-policy-and-session-freshness.md\#Risk is declared at its source, once, by its owner |
| G-SENS-02 | docs/adr/0009-route-policy-and-session-freshness.md\#Risk is declared at its source, once, by its owner |
| G-SENS-03 | docs/adr/0009-route-policy-and-session-freshness.md\#Risk is declared at its source, once, by its owner |
| G-SENS-04 | docs/adr/0009-route-policy-and-session-freshness.md\#What risk triggers (applied by the framework) |
| G-SENS-05 | docs/adr/0009-route-policy-and-session-freshness.md\#What risk triggers (applied by the framework)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Checks (deterministic; each error names its fix) |
| G-SENS-06 | docs/adr/0009-route-policy-and-session-freshness.md\#What risk triggers (applied by the framework)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Checks (deterministic; each error names its fix) |
| G-AUTHZ-01 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Failure rules (fail-closed defaults)<br>docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects |
| G-AUTHZ-02 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authorization<br>docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects |
| G-AUTHZ-03 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#What must stay strict (always a live RPC / local call)<br>docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects |
| G-AUTHZ-04 | docs/modules/index.md\#What you get by default, and what you declare |
| G-AUTHZ-05 | docs/modules/index.md\#What you get by default, and what you declare |
| G-AUTHZ-06 | docs/reference/security-model.md\#Authorization |
| G-AUTHZ-07 | docs/reference/security-model.md\#Authorization |
| G-AUTHZ-08 | docs/reference/security-model.md\#Audit trail |
| G-AUTHZ-09 | docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects<br>tests/fixtures/specs/public-api-1.0-spec.md\#7.2 `of=` (required, no default) |
| G-AUTHZ-10 | docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects<br>tests/fixtures/specs/public-api-1.0-spec.md\#7.2 `of=` (required, no default) |
| G-AUTHZ-11 | docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects |
| G-AUTHZ-12 | docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects |
| G-GUARD-01 | docs/modules/index.md\#What you get by default, and what you declare |
| G-GUARD-02 | docs/reference/security-model.md\#Idempotency |
| G-HTTP-01 | docs/reference/security-model.md\#Transport &amp; request hardening<br>docs/reference/cli.md |
| G-HTTP-02 | docs/reference/cli.md |
| G-HTTP-03 | docs/reference/security-model.md\#Transport &amp; request hardening<br>docs/reference/cli.md |
| G-HTTP-04 | docs/reference/security-model.md\#Transport &amp; request hardening<br>docs/reference/cli.md |
| G-HTTP-05 | docs/reference/security-model.md\#Transport &amp; request hardening<br>docs/reference/security-model.md\#Secrets |
| G-HTTP-06 | docs/reference/security-model.md\#Transport &amp; request hardening<br>docs/reference/cli.md |
| G-HTTP-07 | docs/reference/security-model.md\#Transport &amp; request hardening<br>docs/reference/cli.md |
| G-HTTP-08 | docs/reference/security-model.md\#Transport &amp; request hardening |
| G-HTTP-09 | docs/reference/security-model.md\#Transport &amp; request hardening |
| G-HTTP-10 | docs/reference/security-model.md\#Transport &amp; request hardening<br>docs/contributing/repository.md\#Things that look wrong but are intentional |
| G-HTTP-11 | docs/reference/security-model.md\#Secrets |
| G-HTTP-12 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| G-BILLING-01 | docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Gaps the classification exposed — resolved<br>docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle |
| G-BILLING-02 | docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Gaps the classification exposed — resolved<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Guarantees, precisely<br>docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle |
| G-BILLING-03 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Guarantees, precisely<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Strict RPC is not zero staleness: the TOCTOU window |
| G-BILLING-04 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Intent lifecycle (state transitions) |
| G-BILLING-05 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Gaps the classification exposed — resolved<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Intent lifecycle (state transitions)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Open items (owner questions)<br>docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle |
| G-BILLING-06 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Intent ownership (application-level isolation) |
| G-BILLING-07 | docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Gaps the classification exposed — resolved<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix |
| G-BILLING-08 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Open items (owner questions) |
| G-BILLING-09 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Intent reconciliation (\#72)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Open items (owner questions) |
| G-BILLING-10 | docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Gaps the classification exposed — resolved |
| G-BILLING-11 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix<br>docs/contributing/repository.md\#Things that look wrong but are intentional |
| G-BILLING-12 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix |
| G-BILLING-13 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix |
| G-BILLING-14 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix |
| G-BILLING-15 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Open items (owner questions) |
| G-BILLING-16 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Open items (owner questions) |
| G-BILLING-17 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Webhook matrix<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Open items (owner questions) |
| G-BILLING-18 | docs/reference/cli.md<br>docs/reference/security-model.md\#Secrets<br>docs/operate/topologies.md |
| G-BILLING-19 | docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Gaps the classification exposed — resolved<br>docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle |
| G-BILLING-20 | docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects<br>tests/fixtures/specs/public-api-1.0-spec.md\#7.4 Account selection in billing itself |
| G-BILLING-21 | docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects<br>tests/fixtures/specs/public-api-1.0-spec.md\#7.4 Account selection in billing itself |
| G-BILLING-22 | docs/modules/standard/billing.md |
| G-CONTRACT-01 | docs/modules/between/contracts.md<br>docs/modules/between/contracts.md\#Local equals remote |
| G-CONTRACT-02 | docs/modules/between/contracts.md<br>docs/modules/between/contracts.md\#Local equals remote |
| G-CONTRACT-03 | docs/modules/between/contracts.md<br>docs/modules/between/contracts.md\#Local equals remote |
| G-CONTRACT-04 | docs/modules/between/contracts.md<br>docs/modules/between/contracts.md\#Local equals remote |
| G-CONTRACT-05 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Local == remote |
| G-CONTRACT-06 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Local == remote |
| G-CONTRACT-07 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Local == remote |
| G-CONTRACT-08 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Local == remote |
| G-CONTRACT-09 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Errors |
| G-CONTRACT-10 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Errors |
| G-CONTRACT-11 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Errors |
| G-CONTRACT-12 | docs/modules/index.md\#What you get by default, and what you declare<br>deploy/README.md\#How the split works |
| G-CONTRACT-13 | docs/modules/between/contracts.md<br>docs/modules/between/contracts.md\#Local equals remote |
| G-CONTRACT-14 | docs/modules/between/contracts.md<br>docs/modules/between/contracts.md\#Local equals remote |
| G-CONTRACT-15 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Transport &amp; performance |
| G-CONTRACT-16 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Local == remote<br>docs/operate/service-rpc.md\#Transport &amp; performance |
| G-CONTRACT-17 | docs/operate/service-rpc.md\#Local == remote |
| G-CONTRACT-18 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Request authentication and idempotency |
| G-CONTRACT-19 | docs/modules/between/contracts.md<br>docs/adr/0010-public-api-1.0.md\#Note (2026-10-28): contract DTOs authored in the provider, generated into the DTO package |
| G-COMPAT-01 | docs/modules/between/contracts.md<br>deploy/README.md\#Rolling upgrades: contract compatibility |
| G-COMPAT-02 | docs/modules/between/contracts.md<br>deploy/README.md\#Rolling upgrades: contract compatibility |
| G-COMPAT-03 | docs/reference/cli.md<br>deploy/README.md\#Rolling upgrades: contract compatibility |
| G-COMPAT-04 | docs/modules/between/contracts.md<br>deploy/README.md\#Rolling upgrades: contract compatibility |
| G-COMPAT-05 | docs/modules/between/contracts.md<br>deploy/README.md\#Rolling upgrades: contract compatibility |
| G-COMPAT-06 | docs/modules/between/contracts.md<br>deploy/README.md\#Rolling upgrades: contract compatibility |
| G-COMPAT-07 | deploy/README.md\#Rolling upgrades: contract compatibility |
| G-RPC-01 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Key schedule<br>docs/operate/service-rpc.md\#Request authentication and idempotency |
| G-RPC-02 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Key schedule<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Threat model |
| G-RPC-03 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Request MAC |
| G-RPC-04 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Request MAC<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Errors<br>docs/operate/service-rpc.md\#Request authentication and idempotency |
| G-RPC-05 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Request MAC<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Threat model<br>docs/reference/security-model.md\#Service-to-service trust (internal RPC) |
| G-RPC-06 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Replay<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Third review (re-review of the KCI fix, 2026-10-21) |
| G-RPC-07 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Replay<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Security review |
| G-RPC-08 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Replay<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Second review (independent, 2026-10-21) |
| G-RPC-09 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Replay<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Second review (independent, 2026-10-21) |
| G-RPC-10 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Request authentication and idempotency |
| G-RPC-11 | docs/modules/between/contracts.md<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Downgrade<br>docs/reference/security-model.md\#Service-to-service trust (internal RPC) |
| G-RPC-12 | docs/modules/between/contracts.md<br>docs/reference/security-model.md\#Service-to-service trust (internal RPC) |
| G-RPC-13 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Key-compromise impersonation (KCI) and the command signature<br>docs/reference/security-model.md\#Service-to-service trust (internal RPC) |
| G-RPC-14 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Key-compromise impersonation (KCI) and the command signature<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Key rotation |
| G-RPC-15 | docs/modules/between/contracts.md<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Downgrade<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Amendment (2026-10-25) |
| G-RPC-16 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Provider endpoint<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Amendment (2026-10-25) |
| G-RPC-17 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Key rotation<br>docs/operate/service-rpc.md\#Response binding |
| G-RPC-18 | docs/modules/between/contracts.md<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Errors<br>docs/operate/service-rpc.md\#Errors |
| G-RPC-19 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Response binding<br>docs/reference/security-model.md\#Response binding — threat model |
| G-RPC-20 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Request MAC<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Security review<br>docs/reference/security-model.md\#Response binding — threat model |
| G-RPC-21 | docs/operate/service-rpc.md\#Response binding<br>docs/reference/security-model.md\#Response binding — threat model<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Amendment (2026-10-25) |
| G-RPC-22 | docs/operate/service-rpc.md\#Response binding |
| G-RPC-23 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Transport &amp; performance |
| G-RPC-24 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Transport &amp; performance |
| G-RPC-25 | docs/modules/between/contracts.md<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Replay<br>docs/operate/service-rpc.md\#Request authentication and idempotency |
| G-RPC-26 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Transport &amp; performance |
| G-RPC-27 | docs/operate/service-rpc.md\#Transport &amp; performance |
| G-RPC-28 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Downgrade<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Amendment (2026-10-25)<br>docs/operate/service-rpc.md\#Provider endpoint |
| G-RPC-29 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Amendment (2026-10-25)<br>docs/reference/cli.md |
| G-RPC-30 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Key rotation<br>docs/reference/security-model.md\#Service-to-service trust (internal RPC) |
| G-RPC-31 | docs/adr/0004-rpc-pair-mac-request-authentication.md\#Errors<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Security review |
| G-RPC-32 | docs/operate/service-rpc.md\#Request authentication and idempotency<br>docs/adr/0004-rpc-pair-mac-request-authentication.md\#Second review (independent, 2026-10-21) |
| G-RPC-33 | docs/reference/security-model.md\#Service-to-service trust (internal RPC)<br>deploy/README.md\#Probes (liveness vs readiness) |
| G-MODULE-01 | docs/modules/index.md\#What a module may import<br>docs/modules/index.md\#How modules talk to each other |
| G-MODULE-02 | docs/modules/index.md\#What a module may import<br>docs/adr/0001-framework-distribution.md\#Verification of current boundaries (from the codebase) |
| G-MODULE-03 | docs/modules/index.md\#What a module may import |
| G-MODULE-04 | docs/modules/index.md\#What a module may import |
| G-MODULE-05 | docs/infrastructure/index.md |
| G-MODULE-06 | docs/infrastructure/index.md |
| G-MODULE-07 | docs/modules/database/units.md |
| G-MODULE-08 | docs/modules/database/units.md<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-MODULE-09 | docs/modules/index.md\#What a module may import |
| G-MODULE-10 | docs/modules/index.md\#What you get by default, and what you declare |
| G-MODULE-11 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/contributing/repository.md\#Things that look wrong but are intentional |
| G-MODULE-12 | docs/modules/index.md\#What you get by default, and what you declare |
| G-MODULE-13 | docs/reference/testing.md<br>docs/adr/0002-framework-absorbs-module-boilerplate.md\#Decision |
| G-MODULE-14 | docs/adr/0002-framework-absorbs-module-boilerplate.md\#Decision<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-MODULE-15 | docs/operate/topologies.md |
| G-MODULE-16 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/contributing/repository.md\#Repository map |
| G-MODULE-17 | docs/modules/index.md\#What you get by default, and what you declare |
| G-MODULE-18 | docs/adr/0002-framework-absorbs-module-boilerplate.md\#Decision<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-KERNEL-01 | docs/adr/0007-kernel-layers.md\#Decision<br>docs/adr/0007-kernel-layers.md\#The map (top → bottom)<br>docs/modules/index.md\#What a module may import |
| G-KERNEL-02 | docs/adr/0007-kernel-layers.md\#Decision<br>docs/adr/0007-kernel-layers.md\#The one hook<br>docs/adr/0007-kernel-layers.md\#Principles |
| G-KERNEL-03 | docs/adr/0007-kernel-layers.md\#Decision<br>docs/contributing/repository.md\#Repository map |
| G-KERNEL-04 | docs/adr/0007-kernel-layers.md\#Decision<br>docs/adr/0007-kernel-layers.md\#The default-registry fallback was dropped |
| G-KERNEL-05 | docs/adr/0007-kernel-layers.md\#Function-local imports<br>docs/modules/index.md\#What a module may import |
| G-KERNEL-06 | docs/adr/0007-kernel-layers.md\#Function-local imports<br>docs/adr/0007-kernel-layers.md\#Consequences |
| G-KERNEL-07 | docs/infrastructure/index.md<br>docs/adr/0001-framework-distribution.md\#Verification of current boundaries (from the codebase) |
| G-KERNEL-08 | docs/adr/0007-kernel-layers.md\#Principles |
| G-KERNEL-09 | docs/modules/database/multi-tenancy.md |
| G-KERNEL-10 | docs/modules/index.md\#What a module may import<br>docs/adr/0001-framework-distribution.md\#Verification of current boundaries (from the codebase) |
| G-KERNEL-11 | docs/adr/0001-framework-distribution.md\#3. Compatibility contract<br>docs/adr/0001-framework-distribution.md\#Implementation status<br>docs/adr/0007-kernel-layers.md\#What moved (no re-exports at the old paths) |
| G-KERNEL-12 | docs/reference/testing.md |
| G-KERNEL-13 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/adr/0001-framework-distribution.md\#Boundary remediations required before the split<br>docs/adr/0001-framework-distribution.md\#4. Assembly |
| G-INFRA-01 | docs/infrastructure/index.md<br>docs/adr/0007-kernel-layers.md\#Principles |
| G-INFRA-02 | docs/adr/0007-kernel-layers.md\#The default-registry fallback was dropped<br>docs/infrastructure/index.md |
| G-INFRA-03 | docs/infrastructure/index.md<br>docs/infrastructure/index.md |
| G-INFRA-04 | docs/infrastructure/index.md |
| G-INFRA-05 | docs/infrastructure/index.md |
| G-INFRA-06 | docs/infrastructure/index.md |
| G-INFRA-07 | docs/infrastructure/index.md |
| G-INFRA-08 | docs/adr/0002-framework-absorbs-module-boilerplate.md\#Decision<br>docs/reference/testing.md |
| G-CONFIG-01 | docs/reference/cli.md<br>docs/adr/0006-accepted-risks.md\#Amendment (2026-10-25) |
| G-CONFIG-02 | docs/reference/cli.md<br>docs/adr/0006-accepted-risks.md\#Consequences<br>docs/adr/0006-accepted-risks.md\#Amendment (2026-10-25) |
| G-CONFIG-03 | docs/reference/cli.md<br>docs/adr/0006-accepted-risks.md\#Decision<br>docs/adr/0006-accepted-risks.md\#Amendment (2026-10-25)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Removed from the internal API before the initial public alpha |
| G-CONFIG-04 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Secrets management |
| G-CONFIG-05 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Preflight and effective configuration |
| G-CONFIG-06 | docs/reference/cli.md |
| G-CONFIG-07 | docs/contributing/repository.md\#Repository map |
| G-CONFIG-08 | docs/reference/cli.md |
| G-RISK-01 | docs/adr/0006-accepted-risks.md\#Decision<br>docs/reference/cli.md |
| G-RISK-02 | docs/adr/0006-accepted-risks.md\#Decision<br>docs/reference/cli.md |
| G-RISK-03 | docs/adr/0006-accepted-risks.md\#Amendment (2026-10-25)<br>docs/adr/0006-accepted-risks.md\#Consequences |
| G-RISK-04 | docs/reference/cli.md<br>docs/adr/0006-accepted-risks.md\#Amendment (2026-10-25) |
| G-RISK-05 | docs/adr/0006-accepted-risks.md\#Decision<br>docs/reference/cli.md |
| G-RISK-06 | docs/infrastructure/index.md<br>docs/adr/0006-accepted-risks.md\#Amendment (2026-10-25) |
| G-RISK-07 | docs/reference/cli.md<br>docs/contributing/repository.md\#Things that look wrong but are intentional |
| G-RISK-08 | docs/adr/0006-accepted-risks.md\#Amendment (2026-10-25) |
| G-RISK-09 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Password hashing: work factor &amp; capacity planning |
| G-RISK-10 | docs/reference/cli.md |
| G-RISK-11 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Database migrations |
| G-RISK-12 | docs/modules/database/units.md\#Effects that wait for the commit, checks that run before it<br>docs/adr/0006-accepted-risks.md\#Amendment (2026-10-25) |
| G-MIGRATE-01 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/operate/operator-reference.md\#Database migrations |
| G-MIGRATE-02 | docs/modules/index.md\#What you get by default, and what you declare |
| G-MIGRATE-03 | docs/modules/index.md\#What you get by default, and what you declare |
| G-MIGRATE-04 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Database migrations |
| G-MIGRATE-05 | docs/contributing/repository.md\#Repository map<br>docs/modules/index.md\#What you get by default, and what you declare |
| G-MIGRATE-06 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Database migrations |
| G-MIGRATE-07 | deploy/README.md\#Runbook: moving a module's data out of the monolith database |
| G-MIGRATE-08 | deploy/README.md\#Runbook: moving a module's data out of the monolith database |
| G-MIGRATE-09 | docs/modules/background/tasks.md<br>docs/reference/cli.md |
| G-MIGRATE-10 | docs/operate/operator-reference.md\#Database migrations |
| G-MIGRATE-11 | docs/infrastructure/index.md |
| G-OPS-01 | docs/modules/background/tasks.md<br>deploy/README.md\#Background work (Celery worker + beat) |
| G-OPS-02 | docs/contributing/repository.md\#Things that look wrong but are intentional<br>deploy/README.md\#Probes (liveness vs readiness) |
| G-OPS-03 | docs/operate/topologies.md<br>deploy/README.md\#Probes (liveness vs readiness) |
| G-OPS-04 | docs/reference/cli.md |
| G-OPS-05 | docs/operate/topologies.md<br>deploy/README.md\#Background work (Celery worker + beat) |
| G-OPS-06 | docs/reference/cli.md<br>docs/adr/0006-accepted-risks.md\#Decision<br>deploy/README.md\#Multiworker metrics and shutdown |
| G-OPS-07 | docs/reference/testing.md |
| G-OPS-08 | docs/reference/testing.md |
| G-OPS-09 | docs/reference/testing.md |
| G-OPS-10 | docs/reference/testing.md<br>docs/toolchain/testing.md\#Test isolation |
| G-OPS-11 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Preflight and effective configuration |
| G-OPS-12 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Security incident runbook (database leak / credential leak / key compromise) |
| G-OPS-13 | docs/operate/operator-reference.md\#Observability<br>docs/operate/operator-reference.md\#Data retention &amp; PII |
| G-OPS-14 | docs/operate/topologies.md<br>deploy/README.md\#Config parity invariants |
| G-OPS-15 | docs/operate/topologies.md<br>deploy/README.md\#Gateway and client IPs (X-Forwarded-For) |
| G-OPS-16 | docs/operate/topologies.md<br>deploy/README.md\#Migrations (one-shot `migrate` services) |
| G-OPS-17 | docs/modules/background/tasks.md<br>deploy/README.md\#Background work (Celery worker + beat) |
| G-OPS-18 | docs/operate/operator-reference.md\#Backups &amp; restore |
| G-OPS-19 | docs/operate/operator-reference.md\#Restoring: the order, and the traffic gate<br>docs/operate/operator-reference.md\#Backups &amp; restore |
| G-OPS-20 | docs/operate/topologies.md |
| G-OPS-21 | docs/reference/cli.md<br>deploy/README.md\#Required modules |
| G-OPS-22 | docs/operate/split-into-services.md<br>docs/reference/security-model.md |
| G-SAGA-01 | docs/adr/0008-sagas.md\#1. Definition (registration-time checks) |
| G-SAGA-02 | docs/adr/0008-sagas.md\#1. Definition (registration-time checks) |
| G-SAGA-03 | docs/adr/0008-sagas.md\#2. State: two core-chain tables in the owning module's database |
| G-SAGA-04 | docs/adr/0008-sagas.md\#3. Operation keys |
| G-SAGA-05 | docs/adr/0008-sagas.md\#4. Start: transactional with the business change |
| G-SAGA-06 | docs/adr/0008-sagas.md\#5. Driving: an outbox-fed consumer plus a timer sweep |
| G-SAGA-07 | docs/adr/0008-sagas.md\#6. The step protocol (one UoW per step) |
| G-SAGA-08 | docs/adr/0008-sagas.md\#5. Driving: an outbox-fed consumer plus a timer sweep |
| G-SAGA-09 | docs/adr/0008-sagas.md\#7. Recovery rules |
| G-SAGA-10 | docs/adr/0008-sagas.md\#7. Recovery rules |
| G-SAGA-11 | docs/adr/0008-sagas.md\#7. Recovery rules |
| G-SAGA-12 | docs/adr/0008-sagas.md\#8. Operations |
| G-SAGA-13 | docs/adr/0008-sagas.md\#9. Effect-model amendment (ADR-0005 §4, one new row; no new record) |
| G-SAGA-14 | docs/adr/0008-sagas.md\#11. Versioning in-flight instances |
| G-SAGA-15 | docs/adr/0008-sagas.md\#8. Operations |
| G-SDK-01 | tests/fixtures/specs/public-api-1.0-spec.md\#2.4 Import isolation<br>docs/adr/0010-public-api-1.0.md\#1. One public namespace, `sandloom.sdk`, with a beginner tier |
| G-SDK-02 | tests/fixtures/specs/public-api-1.0-spec.md\#3. Three API levels<br>docs/adr/0010-public-api-1.0.md\#2. Three API levels; level 1 is built only from level 2 |
| G-SDK-03 | tests/fixtures/specs/public-api-1.0-spec.md\#3. Three API levels<br>docs/adr/0010-public-api-1.0.md\#2. Three API levels; level 1 is built only from level 2 |
| G-SDK-04 | tests/fixtures/specs/public-api-1.0-spec.md\#2.2 Modules |
| G-SDK-05 | tests/fixtures/specs/public-api-1.0-spec.md\#3. Three API levels |
| G-DECL-01 | tests/fixtures/specs/public-api-1.0-spec.md\#5.2 Typed events |
| G-DECL-02 | tests/fixtures/specs/public-api-1.0-spec.md\#5.4 Consumers |
| G-DECL-03 | tests/fixtures/specs/public-api-1.0-spec.md\#5.3 Typed contracts |
| G-DECL-04 | tests/fixtures/specs/public-api-1.0-spec.md\#5.3 Typed contracts |
| G-DECL-05 | tests/fixtures/specs/public-api-1.0-spec.md\#5.10 No function-local imports in modules |
| G-DECL-06 | tests/fixtures/specs/public-api-1.0-spec.md\#5.2 Typed events |
| G-DECL-07 | docs/modules/between/contracts.md<br>docs/adr/0010-public-api-1.0.md\#Note (2026-10-28): contract DTOs authored in the provider, generated into the DTO package |
| G-MODULE-19 | tests/fixtures/specs/public-api-1.0-spec.md\#5.7 The application manifest and discovery |
| G-MIGRATE-12 | tests/fixtures/specs/public-api-1.0-spec.md\#5.6 Generated migrations with RLS<br>tests/fixtures/specs/public-api-1.0-spec.md\#5.8 `sandloom test` |
| G-AUTHZ-13 | tests/fixtures/specs/public-api-1.0-spec.md\#8.3 The authorization algebra is unchanged (Q4 revised twice)<br>tests/fixtures/specs/public-api-1.0-spec.md\#8.2 Property-based (Hypothesis) |
| G-SENS-07 | docs/adr/0009-route-policy-and-session-freshness.md\#Risk is declared at its source, once, by its owner<br>tests/fixtures/specs/public-api-1.0-spec.md\#8.2 Property-based (Hypothesis) |
| C-TENANT-01 | docs/modules/database/multi-tenancy.md |
| C-TENANT-02 | docs/modules/database/multi-tenancy.md |
| C-TENANT-03 | docs/modules/database/multi-tenancy.md<br>docs/reference/security-model.md\#Tenant isolation |
| C-TENANT-04 | docs/modules/database/multi-tenancy.md |
| C-TENANT-05 | docs/modules/database/multi-tenancy.md<br>docs/reference/security-model.md\#Tenant RLS threat model |
| C-TENANT-06 | docs/reference/security-model.md\#The system channel (privileged work) |
| C-TENANT-07 | docs/reference/security-model.md\#Production profile: the multi-tenant trust model as a contract |
| C-TENANT-08 | docs/reference/security-model.md\#Security-critical vs best-effort cache |
| C-TENANT-09 | docs/modules/database/multi-tenancy.md |
| C-EVENT-01 | docs/modules/index.md\#What you get by default, and what you declare |
| C-EVENT-02 | docs/modules/index.md\#What a module may import |
| C-EVENT-03 | docs/modules/index.md\#What you get by default, and what you declare |
| C-EFFECT-01 | docs/modules/background/tasks.md |
| C-EFFECT-02 | docs/modules/database/units.md<br>docs/adr/0005-effect-model.md\#3. The commit pipeline — one stage list |
| C-AUTH-01 | docs/reference/security-model.md\#Authentication |
| C-AUTH-02 | docs/reference/security-model.md\#Authentication |
| C-AUTH-03 | docs/reference/security-model.md\#Authentication<br>docs/operate/topologies.md |
| C-AUTH-04 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authentication |
| C-AUTH-05 | docs/modules/standard/identity.md\#Behaviour API clients must know |
| C-AUTH-06 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/reference/security-model.md\#Authorization<br>docs/adr/0010-public-api-1.0.md\#7. Explicit authorization subjects |
| C-AUTH-07 | docs/modules/database/units.md<br>docs/reference/security-model.md\#Authority at commit (admin and money effects)<br>docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| C-AUTH-08 | docs/adr/0003-fewer-sync-rpc-membership-reads.md\#Intent reconciliation (\#72) |
| C-AUTH-09 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare)<br>tests/fixtures/specs/public-api-1.0-spec.md\#2.3 New names: exact signatures |
| C-ROUTE-01 | docs/adr/0009-route-policy-and-session-freshness.md\#Defaults every route gets (nothing to declare) |
| C-ROUTE-02 | docs/adr/0009-route-policy-and-session-freshness.md\#Risk is declared at its source, once, by its owner |
| C-UNIT-01 | docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.3 `db()` ownership |
| C-UNIT-02 | docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle<br>tests/fixtures/specs/public-api-1.0-spec.md\#4.3 `db()` ownership |
| C-SPLIT-01 | docs/operate/topologies.md<br>deploy/README.md\#How the split works |
| C-SPLIT-02 | docs/operate/topologies.md<br>docs/operate/service-rpc.md\#Resolution order |
| C-SPLIT-03 | docs/operate/topologies.md |
| C-SPLIT-04 | docs/operate/topologies.md<br>deploy/README.md\#Probes (liveness vs readiness) |
| C-SPLIT-05 | deploy/README.md\#How the split works |
| C-SPLIT-06 | docs/operate/topologies.md<br>docs/operate/topologies.md<br>deploy/README.md\#How the split works |
| C-SPLIT-07 | docs/modules/index.md\#What you get by default, and what you declare<br>docs/operate/topologies.md |
| C-RPC-01 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Transport &amp; performance |
| C-RPC-02 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Transport &amp; performance |
| C-RPC-03 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Request authentication and idempotency |
| C-RPC-04 | docs/reference/security-model.md\#Service-to-service trust (internal RPC) |
| C-RPC-05 | docs/modules/between/contracts.md<br>docs/operate/service-rpc.md\#Local == remote |
| C-INFRA-01 | docs/infrastructure/index.md |
| C-INFRA-02 | docs/infrastructure/index.md<br>docs/adr/0001-framework-distribution.md\#2. Package graph (monorepo + uv workspace, N wheels, independent versions) |
| C-INFRA-03 | docs/infrastructure/index.md |
| C-INFRA-04 | docs/infrastructure/index.md<br>docs/modules/database/units.md |
| C-INFRA-05 | docs/infrastructure/index.md |
| C-INFRA-06 | docs/infrastructure/index.md |
| C-INFRA-07 | docs/infrastructure/index.md |
| C-INFRA-08 | docs/infrastructure/index.md |
| C-INFRA-09 | docs/infrastructure/index.md |
| C-INFRA-10 | docs/infrastructure/index.md |
| C-INFRA-11 | docs/infrastructure/index.md |
| C-INFRA-12 | docs/infrastructure/index.md<br>docs/adr/0010-public-api-1.0.md\#5. One writer per request |
| C-MODULE-01 | docs/modules/index.md\#What you get by default, and what you declare |
| C-MODULE-02 | docs/adr/0001-framework-distribution.md\#5. Scaffold mechanics (`sandloom new`)<br>docs/modules/index.md\#What you get by default, and what you declare |
| C-MODULE-03 | docs/adr/0001-framework-distribution.md\#1. Consumption model — three tiers<br>docs/adr/0001-framework-distribution.md\#2. Package graph (monorepo + uv workspace, N wheels, independent versions)<br>docs/adr/0001-framework-distribution.md\#4. Assembly |
| C-MODULE-04 | docs/adr/0002-framework-absorbs-module-boilerplate.md\#Decision<br>docs/adr/0002-framework-absorbs-module-boilerplate.md\#Consequences<br>docs/modules/index.md\#What you get by default, and what you declare |
| C-MODULE-05 | docs/reference/cli.md<br>docs/modules/index.md\#What a module may import |
| C-MODULE-06 | docs/operate/topologies.md<br>docs/reference/cli.md |
| C-MODULE-07 | docs/reference/cli.md |
| C-MODULE-08 | docs/adr/0002-framework-absorbs-module-boilerplate.md\#Decision<br>docs/adr/0002-framework-absorbs-module-boilerplate.md\#Consequences<br>docs/reference/testing.md |
| C-MODULE-09 | docs/adr/0002-framework-absorbs-module-boilerplate.md\#Decision<br>docs/modules/index.md\#What you get by default, and what you declare |
| C-MIGRATE-01 | docs/reference/cli.md<br>deploy/README.md\#Migrations (one-shot `migrate` services) |
| C-MIGRATE-02 | docs/reference/cli.md |
| C-MIGRATE-03 | docs/reference/cli.md |
| C-MIGRATE-04 | docs/modules/index.md\#What you get by default, and what you declare |
| C-MIGRATE-05 | docs/reference/cli.md |
| C-MIGRATE-06 | docs/reference/cli.md<br>deploy/README.md\#Runbook: moving a module's data out of the monolith database |
| C-MIGRATE-07 | docs/reference/cli.md |
| C-MIGRATE-08 | docs/modules/index.md\#What you get by default, and what you declare |
| C-PROJ-01 | docs/modules/between/projections.md |
| C-PROJ-02 | docs/modules/between/projections.md |
| C-PROJ-03 | docs/modules/between/projections.md<br>docs/reference/cli.md |
| C-PROJ-04 | docs/modules/between/projections.md |
| C-PROJ-05 | docs/modules/between/projections.md |
| C-PROJ-06 | docs/modules/between/projections.md |
| C-PROJ-07 | docs/modules/between/projections.md<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.4 Projection convergence model |
| C-PROJ-08 | docs/modules/between/projections.md<br>docs/modules/between/add-projection.md |
| C-MSTORE-01 | docs/adr/0010-public-api-1.0.md\#6. Guarantees as a lattice, labels derived from mechanisms (amends ADR-0005) |
| C-MSTORE-02 | docs/adr/0010-public-api-1.0.md\#6. Guarantees as a lattice, labels derived from mechanisms (amends ADR-0005) |
| C-MSTORE-03 | docs/adr/0010-public-api-1.0.md\#6. Guarantees as a lattice, labels derived from mechanisms (amends ADR-0005)<br>tests/fixtures/specs/public-api-1.0-spec.md\#6.3 Labels are derived from the mechanism |
| C-MSTORE-04 | docs/adr/0010-public-api-1.0.md\#6. Guarantees as a lattice, labels derived from mechanisms (amends ADR-0005)<br>docs/infrastructure/index.md |
| C-OPS-01 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Events: outbox relay and dead letters |
| C-OPS-02 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Events: outbox relay and dead letters |
| C-OPS-03 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Events: outbox relay and dead letters |
| C-OPS-04 | docs/reference/cli.md |
| C-OPS-05 | docs/reference/cli.md<br>deploy/README.md\#Rolling upgrades: contract compatibility |
| C-OPS-06 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Backups &amp; restore |
| C-OPS-07 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Restoring: the order, and the traffic gate |
| C-OPS-08 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Security incident runbook (database leak / credential leak / key compromise) |
| C-OPS-09 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Preflight and effective configuration |
| C-OPS-10 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Preflight and effective configuration |
| C-OPS-11 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Secrets management |
| C-OPS-12 | docs/operate/operator-reference.md\#Database migrations<br>docs/operate/operator-reference.md\#Preflight and effective configuration |
| C-OPS-13 | docs/reference/cli.md |
| C-OPS-14 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Secrets management |
| C-RISK-01 | docs/adr/0006-accepted-risks.md\#Decision<br>docs/reference/cli.md |
| C-DEPLOY-01 | docs/operate/topologies.md<br>deploy/README.md\#Profiles |
| C-DEPLOY-02 | docs/operate/topologies.md<br>deploy/README.md\#How the split works |
| C-DEPLOY-03 | deploy/README.md\#Smoke test (CI: `microservices-smoke`) |
| C-DEPLOY-04 | deploy/README.md\#Sizing<br>deploy/README.md\#Separate idempotency Redis (opt-in) |
| C-DEPLOY-05 | docs/operate/topologies.md<br>deploy/README.md\#Sizing |
| C-OBS-01 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Observability |
| C-OBS-02 | docs/operate/operator-reference.md\#Operational metrics<br>deploy/README.md\#Multiworker metrics and shutdown |
| C-OBS-03 | docs/operate/operator-reference.md\#Operational metrics |
| C-OBS-04 | docs/operate/operator-reference.md\#Operational metrics |
| C-OBS-05 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Observability |
| C-OBS-06 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Observability |
| C-OBS-07 | docs/reference/cli.md<br>docs/operate/operator-reference.md\#Garbage collector tuning |
| C-OBS-08 | docs/operate/operator-reference.md\#Alerts |
| C-CONSOLE-01 | docs/admin-console/index.md |
| C-CONSOLE-02 | docs/admin-console/deploy.md |
| C-CONSOLE-03 | docs/admin-console/add-your-module.md |
| C-CONSOLE-04 | docs/admin-console/index.md |
| C-TEST-01 | docs/reference/testing.md |
| C-TEST-02 | docs/reference/cli.md<br>docs/reference/testing.md |
| C-TEST-03 | docs/reference/cli.md |
| C-TEST-05 | docs/reference/cli.md |
| C-TEST-06 | docs/reference/testing.md |
| C-WORKER-01 | docs/modules/background/tasks.md<br>deploy/README.md\#Background work (Celery worker + beat) |
| C-WORKER-02 | docs/modules/background/tasks.md |
| C-WORKER-03 | docs/operate/topologies.md<br>deploy/README.md\#Sizing |
| C-WORKER-04 | docs/modules/background/tasks.md |
| C-WORKER-05 | docs/modules/index.md\#What you get by default, and what you declare |
| C-WORKER-06 | docs/modules/background/tasks.md<br>deploy/README.md\#Background work (Celery worker + beat) |
| C-SAGA-01 | docs/adr/0008-sagas.md\#1. Definition (registration-time checks) |
| C-SAGA-02 | docs/adr/0008-sagas.md\#8. Operations |
| C-SAGA-03 | docs/adr/0008-sagas.md\#13. Testing |
| C-DECL-01 | tests/fixtures/specs/public-api-1.0-spec.md\#2.3 New names: exact signatures |
| C-DECL-02 | tests/fixtures/specs/public-api-1.0-spec.md\#5.3 Typed contracts |
| C-DECL-03 | tests/fixtures/specs/public-api-1.0-spec.md\#5.1 Model E and the module router |
| C-DECL-04 | tests/fixtures/specs/public-api-1.0-spec.md\#6.4 Projection convergence model |
| C-DECL-05 | docs/adr/0010-public-api-1.0.md\#4. Transaction and replay lifecycle<br>tests/fixtures/specs/public-api-1.0-spec.md\#5.5 `db()` |
| C-SDK-01 | tests/fixtures/specs/public-api-1.0-spec.md\#2.5 Zero kernel reads |
| C-SDK-02 | tests/fixtures/specs/public-api-1.0-spec.md\#2.5 Zero kernel reads |
| C-GEN-01 | tests/fixtures/specs/public-api-1.0-spec.md\#5.9 Generators |
| C-GEN-02 | tests/fixtures/specs/public-api-1.0-spec.md\#5.9 Generators |
| C-GEN-03 | tests/fixtures/specs/public-api-1.0-spec.md\#5.9 Generators |
| C-GEN-04 | tests/fixtures/specs/public-api-1.0-spec.md\#5.9 Generators |
| C-GEN-05 | tests/fixtures/specs/public-api-1.0-spec.md\#5.9 Generators |
| C-GEN-06 | tests/fixtures/specs/public-api-1.0-spec.md\#5.9 Generators |
| C-GEN-07 | tests/fixtures/specs/public-api-1.0-spec.md\#5.6 Generated migrations with RLS |
| C-TEST-07 | tests/fixtures/specs/public-api-1.0-spec.md\#2.3 New names: exact signatures<br>docs/adr/0009-route-policy-and-session-freshness.md\#The route snapshot |
| C-TEST-08 | tests/fixtures/specs/public-api-1.0-spec.md\#2.3 New names: exact signatures |
| C-TEST-09 | tests/fixtures/specs/public-api-1.0-spec.md\#2.3 New names: exact signatures |
| C-TEST-10 | tests/fixtures/specs/public-api-1.0-spec.md\#2.3 New names: exact signatures<br>docs/reference/testing.md |
| C-TEST-11 | docs/reference/testing.md |
| C-TEST-12 | docs/reference/testing.md |
| C-ROUTE-03 | docs/adr/0009-route-policy-and-session-freshness.md\#Checks (deterministic; each error names its fix)<br>tests/fixtures/specs/public-api-1.0-spec.md\#5.1 Model E and the module router |
| C-DEPLOY-06 | deploy/README.md<br>docs/modules/background/tasks.md |
| C-DEPLOY-07 | docs/operate/split-into-services.md<br>deploy/README.md |
| C-DEPLOY-08 | docs/operate/split-into-services.md<br>deploy/AGENTS.md |
| C-DEPLOY-09 | docs/operate/split-into-services.md<br>deploy/README.md |
| C-TEST-13 | docs/reference/testing.md |
| C-TEST-14 | docs/reference/error-codes.md |
| C-TEST-15 | docs/reference/testing.md |
| C-TENANT-10 | docs/modules/database/multi-tenancy.md |
| C-GEN-08 | docs/modules/create.md |
| C-GEN-09 | docs/modules/background/tasks.md |
| C-GEN-10 | docs/getting-started/index.md |
| C-GEN-11 | docs/modules/create.md<br>docs/modules/database/conventions.md\#Timestamps, ids and list order |
| C-GEN-12 | docs/getting-started/index.md |
| C-SDK-04 | tests/fixtures/specs/public-api-1.0-spec.md\#2.5 Zero kernel reads |
| C-SDK-05 | tests/fixtures/specs/public-api-1.0-spec.md\#2.5 Zero kernel reads |
| C-SDK-06 | docs/modules/database/conventions.md\#Timestamps, ids and list order |
| C-GEN-13 | docs/getting-started/index.md |
| C-GEN-14 | examples/README.md |
| C-GEN-15 | docs/reference/cli.md |
| C-MODULE-10 | docs/modules/http/authorization.md |
| C-MODULE-11 | docs/reference/testing.md |
| C-MODULE-12 | docs/modules/index.md<br>docs/reference/error-codes.md\#sandloom-b020 |
| C-MODULE-13 | packages/README.md<br>docs/adr/0001-framework-distribution.md\#2. Package graph (monorepo + uv workspace, N wheels, independent versions) |
| C-SDK-03 | docs/modules/background/tasks.md |
| C-MIGRATE-09 | docs/modules/database/write-migration.md |
| C-PROJ-09 | docs/modules/between/projections.md |
| C-EVENT-04 | docs/modules/standard/tenancy.md<br>docs/modules/standard/billing.md |

## Related reading

- [Guarantee boundaries](<https://sandloom.dev/docs/modules/guarantees/index.md>)
- [Error catalogue](<https://sandloom.dev/docs/reference/error-codes/index.md>)
