# Release checklist — C-CARE v1.0.0 Release Candidate 1

Baseline: `1a952d0` (`feat: complete end-to-end service workflow UAT`).
Status values: **PASS**, **BLOCKED**, **MANUAL**, **DEFERRED**, **NOT APPLICABLE**.

`BLOCKED` means the release may not be declared READY. `MANUAL` means a human must
perform and record the step on the target host. `DEFERRED` means a known,
accepted limitation that does not prevent RC1.

---

## Code

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 1.1 | Phases through 5C frozen; no frozen domain semantics redesigned | PASS | Phase 5D adds configuration, probes, error pages and documentation only; no model, service or permission changed |
| 1.2 | No new business module introduced | PASS | No app added to `INSTALLED_APPS` |
| 1.3 | No production deployment, push or tag performed | PASS | No remote operation; `git tag` not created |
| 1.4 | Working tree was clean at phase start | PASS | Baseline `1a952d0` committed |
| 1.5 | Phase 5D changes are uncommitted for review | PASS | Intended; nothing staged, committed or pushed |

## Migrations

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 2.1 | No model drift | PASS | `makemigrations --check` reports no changes |
| 2.2 | Migration graph valid, no conflicts | PASS | `MigrationLoader.detect_conflicts()` empty; `check_consistent_history` clean |
| 2.3 | Every repository migration is applied | PASS | 41 on disk, 41 applied, 0 missing, 0 orphaned |
| 2.4 | No historical migration rewritten | PASS | 0 migration files modified after the commit that introduced them, across all history |
| 2.5 | No new migration created by this phase | PASS | Phase 5D introduced no model change |
| 2.6 | Production migration procedure documented | PASS | `PRODUCTION_DEPLOYMENT.md` §6; `OPERATIONS_RUNBOOK.md` §Migrations |
| 2.7 | Live production database not migrated | NOT APPLICABLE | No production database in scope |

## Configuration

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 3.1 | `DEBUG=False` in production | PASS | `config/settings/production.py`; `test_production_hardening_is_unconditional` |
| 3.2 | `SECRET_KEY` from the environment, validated | PASS | Weak, short, `django-insecure-` and placeholder values refused at import |
| 3.3 | `ALLOWED_HOSTS` explicit, wildcard refused | PASS | Empty or `*` refused at import |
| 3.4 | `CSRF_TRUSTED_ORIGINS` from the environment | PASS | Empty list when unset; documented as required for cross-origin |
| 3.5 | Secure cookies | PASS | `SESSION_COOKIE_SECURE`, `SESSION_COOKIE_HTTPONLY`, `CSRF_COOKIE_SECURE` all `True` |
| 3.6 | HTTPS redirect enforced | PASS | `SECURE_SSL_REDIRECT=True` |
| 3.7 | HSTS strategy documented and operator-controlled | PASS | 1 year always on; subdomain and preload are explicit opt-ins |
| 3.8 | Proxy SSL header handled, spoofing not possible | PASS | `DJANGO_TRUSTED_PROXY_HEADER` opt-in, validated enum, default `NONE`; proxy must strip client headers (documented) |
| 3.9 | Clickjacking protection | PASS | `X_FRAME_OPTIONS="DENY"` |
| 3.10 | MIME/content-type protection | PASS | `SECURE_CONTENT_TYPE_NOSNIFF=True` |
| 3.11 | Referrer policy | PASS | `DJANGO_SECURE_REFERRER_POLICY`, validated, default `same-origin` |
| 3.12 | Database configuration | PASS | PostgreSQL only; `connect_timeout=5`; database password now accepted from the process environment |
| 3.13 | Timezone | PASS | `Asia/Dhaka`, `USE_TZ=True` |
| 3.14 | Invalid configuration fails closed | PASS | Invalid proxy header, log level and referrer policy all raise `ImproperlyConfigured` |
| 3.15 | Real deployment hostnames not hard-coded | PASS | Every host-related value is an environment variable; docs use placeholders |

## Security

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 4.1 | No tracked `.env`, key, certificate or private key | PASS | 483 tracked files scanned; only `.env.example` present; no key material in tree or history |
| 4.2 | No real credential, token or API key in the repository | PASS | No AWS/GitHub/Slack/OpenAI patterns; one password literal, in a test asserting it is never echoed |
| 4.3 | No real customer data | PASS | Only RFC 2606 `.invalid` domains; no consumer mailbox domains |
| 4.4 | `.env.example` contains names and placeholders only | PASS | Every credential field is blank or `replace-me` |
| 4.5 | `.gitignore` excludes secrets, media, static output, dumps | PASS | `.env`, `.env.*` (except `.env.example`), `media/`, `staticfiles/`, `*.sql`, `*.dump` |
| 4.6 | Authentication | PASS | `apps.access` suite, 119 tests |
| 4.7 | Authorization and company isolation | PASS | Cross-company 404/403 assertions in the Phase 5C UAT suite |
| 4.8 | Region / service-center / department scope | PASS | `test_region_center_and_department_scope_at_new_boundaries` |
| 4.9 | IDOR on every new mutation endpoint | PASS | Six persona/operation pairs, GET and POST, in-company 403 and cross-company 404 |
| 4.10 | CSRF | PASS | POST without a token is 403 on the operational workspace and Admin |
| 4.11 | POST-only mutation endpoints | PASS | PUT returns 405; probes reject POST |
| 4.12 | Password handling | PASS | Four validators; `set_password`/`check_password`; no password email; no self-service reset |
| 4.13 | Output escaping | PASS | Template autoescape on; no `\|safe` on untrusted input; `innerHTML` absent from the workspace script |
| 4.14 | Uploaded files | NOT APPLICABLE | No upload-bearing model in RC1; proxy rule documented for when one ships |
| 4.15 | Unsafe redirects | PASS | `next` handled by Django `LoginView`; `ALLOWED_HOSTS` restricts hosts |
| 4.16 | Configuration exposure over HTTP | PASS | `/health/` and `/ready/` return only `{"status": ...}`; the release version is not exposed |

## Static and media

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 5.1 | `collectstatic` succeeds | PASS | 129 files collected to a temporary `STATIC_ROOT` and verified |
| 5.2 | Temporary target removed afterwards | PASS | Confirmed |
| 5.3 | `STATIC_ROOT` and `STATICFILES_DIRS` configured | PASS | `staticfiles/` and `static/`, both git-ignored |
| 5.4 | Static serving responsibility assigned to the proxy | PASS | Documented in `PRODUCTION_DEPLOYMENT.md` §10 |
| 5.5 | `MEDIA_ROOT` configured | PASS | `media/`, git-ignored, empty by design |
| 5.6 | Media serving responsibility documented | PASS | Proxy rule documented in advance |
| 5.7 | Filesystem permission expectations documented | PASS | `umask 027`, service account ownership, systemd `ReadWritePaths` |
| 5.8 | Django dev static/media serving not assumed in production | PASS | No `django.contrib.staticfiles` URL serving; DEBUG is False |
| 5.9 | Existing user files deleted | NOT APPLICABLE | No user files exist |

## Database

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 6.1 | Single PostgreSQL database, no hidden replicas | PASS | One `default` alias; `audit_project` fails safely otherwise |
| 6.2 | Application role is not a superuser | PASS | `rolsuper = false` |
| 6.3 | Least-privilege role guidance documented | PASS | `PRODUCTION_DEPLOYMENT.md` §4 |
| 6.4 | Database monitoring documented | PASS | `OPERATIONS_RUNBOOK.md` §Database monitoring |
| 6.5 | Connection sizing documented | PASS | Workers × threads + headroom |

## Backup and restore

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 7.1 | `pg_dump` procedure documented | PASS | Custom format, dedicated role, no-owner |
| 7.2 | `pg_restore` procedure documented | PASS | Scratch-verify-then-promote sequence |
| 7.3 | Ownership and permissions guidance | PASS | Separate application and backup roles |
| 7.4 | Retention recommendation | PASS | 24 hourly, 14 daily, 12 monthly |
| 7.5 | Off-host storage required | PASS | Stated as mandatory, not optional |
| 7.6 | Encryption consideration | PASS | At rest in object storage or on-disk encryption, key off-host |
| 7.7 | Restoration verification | PASS | `pg_restore --list` plus the full drill below |
| 7.8 | Media/file backup | NOT APPLICABLE | Nothing to back up in RC1; rule documented for future uploads |
| 7.9 | **Backup/restore drill executed** | PASS | Schema drill: dump → drop → restore → `check` → `makemigrations --check` → readiness → identical 59 migrations and 114 tables. Data drill: seeded synthetic company/customer/device/case → dump → drop → restore → identical row counts and job number, `check` clean, no drift, readiness OK |
| 7.10 | Drill never touched `ccare_dev` | PASS | `pg_dump` is read-only; `ccare_dev` verified at 59 migrations after the drill |
| 7.11 | Drill artifacts cleaned up | PASS | Both temporary databases and both dumps removed; only `ccare_dev` and one pre-existing `audit_*` database remain |
| 7.12 | Live production database never touched | NOT APPLICABLE | No production database in scope |

## Communications

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 8.1 | Outbound delivery disabled by default | PASS | `COMMUNICATIONS_ALLOW_EXTERNAL=False` |
| 8.2 | Production provider required per channel | PASS | Unconfigured channel raises `ImproperlyConfigured` rather than succeeding silently |
| 8.3 | Email production adapter available | PASS | `DjangoEmailProvider`, `production_ready = True` |
| 8.4 | Email configuration validated | PASS | TLS/SSL exclusivity, credential pairing, port and timeout bounds |
| 8.5 | Delivery outcomes definite | PASS | Accepted / rejected / failed; no ambiguous reporting, no silent retry |
| 8.6 | Delivery failure cannot corrupt business data | PASS | Verified in Phase 5C UAT: a failed send leaves the recorded workflow intact |
| 8.7 | **SMS vendor** | DEFERRED | No production SMS adapter ships in RC1. `SmsProvider` is an abstract contract with a fake implementation used only in tests. SMS must remain unconfigured |
| 8.8 | Delivery-status reconciliation with the provider | DEFERRED | C-CARE records acceptance only; inbound delivery receipts are a provider capability, not an RC1 requirement |
| 8.9 | Real communication sent during this phase | NOT APPLICABLE | No external delivery attempted |

## UAT

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 9.1 | End-to-end journey through non-staff operational UI | PASS | `docs/UAT_VERIFICATION.md` §2; 22 journeys |
| 9.2 | Variant journeys | PASS | 21 variant journeys covering rework, rejection, warranty, reversal, release, clearance, reconciliation |
| 9.3 | Reconciliation across all five domains | PASS | Inventory, commercial, SLA, communications, reporting |
| 9.4 | Gaps classified, none invented | PASS | `docs/UAT_GAP_REGISTER.md`: 5 verified gaps, 22 verified passes, 5 out of scope |
| 9.5 | Unresolved gaps | PASS | None; all five fixed and verified |

## Tests

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 10.1 | Phase 5D production-surface tests | PASS | 33 tests: settings guards, environment secrets, health, readiness, error pages, logging, release identity |
| 10.2 | Access security suite | PASS | 119 tests |
| 10.3 | Domain suites | PASS | 1,083 tests across service, devices, customers, organization, communications, sla, frontdesk |
| 10.4 | **Full regression (`audit_project --full`)** | BLOCKED (historical; superseded by 10.12) | Single run: 2,352 tests in 4,598s, **3 failures**. One fixed in Phase 5D (item 10.7). Two remain open and are pre-existing — see Release blocker 1 |
| 10.5 | Destructive penetration testing | NOT APPLICABLE | Deliberately not performed |
| 10.6 | Suite duplicated for release | NOT APPLICABLE | Existing security tests reused rather than re-implemented |
| 10.7 | Non-deterministic ledger assertion in the Phase 5C journey | PASS | The reconciliation assertion ordered two movements sharing a `posted_at` by UUID. Rewritten to group entries per movement and compare sorted shapes; three consecutive runs pass |
| 10.8 | Shared-shell capability regression guards (Phase 5D.1) | PASS | `tests/test_shell_capabilities.py`, 26 tests. See Release blocker 1 for the measured query profile these guards lock in |
| 10.9 | **Second full audit (`audit_project --full`)** | BLOCKED (historical) | Second full run, executed once after the focused suites were green, because Phase 5D.1 was chartered against this blocker. **2,378 tests in 6,939s (audit elapsed 7,004s); 2 failures** — exactly the two pre-existing budget tests, still reporting 58 and 38. The Phase 5D ledger-assertion failure is gone; no new failure introduced. Discovered count rose by exactly the 26 new Phase 5D.1 tests |
| 10.10 | Report round-trip equivalence and regression guards (Phase 5D.3) | PASS | `tests/test_reporting_round_trips.py`, 19 tests: verbatim-legacy oracles for access, ratios, cards, pagination (every table of every builder), scope isolation, export enforcement, extreme-page guard, shell capability identifiers and `visible()` truth table |
| 10.11 | Third full audit (Phase 5D.3 RC verification attempt) | BLOCKED (historical) | 2,416 tests; 5,381.699s test runtime; 5,424.478s elapsed; **1 failure, 9 errors**: extreme `page` reached PostgreSQL as an out-of-range OFFSET (one primary `DataError` plus cascading `InFailedSqlTransaction` errors) and a 21-vs-20 query-stability failure. Both fixed — see Release blocker 1, Phase 5D.3 |
| 10.12 | **Fourth full audit (Phase 5D.3 post-fix)** | **PASS** | **2,419 tests, 0 failures, 0 errors**; 5,314.067s test runtime; 5,367.967s elapsed; all audit checks passed. Operator confirmation run of the same tree: 2,419 tests, 0 failures, 0 errors; 5,415.298s test runtime; 5,462.410s elapsed; `PASS: Full regression`, `AUDIT CHECKS PASSED` |

## Documentation

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 11.1 | Release inventory recorded | PASS | `docs/RELEASE_CHECKLIST.md` and the phase report |
| 11.2 | Deployment runbook | PASS | `docs/PRODUCTION_DEPLOYMENT.md`, 16 ordered steps plus rollback |
| 11.3 | Operations runbook | PASS | `docs/OPERATIONS_RUNBOOK.md`: start/stop, logs, migrations, backup, restore, disk, database, communications, user recovery, common failures, incident response |
| 11.4 | Production topology documented | PASS | `PRODUCTION_DEPLOYMENT.md` §0 and §10; vendor-neutral |
| 11.5 | Backup/restore runbook | PASS | `OPERATIONS_RUNBOOK.md` §Backup, §Restore, §Drill |
| 11.6 | Existing documentation still accurate | PASS | `DEVELOPMENT.md` updated for the environment-supplied database password |
| 11.7 | Release identity documented | PASS | `RELEASE_NAME` / `RELEASE_VERSION` in settings; RC1 named in the runbooks |
| 11.8 | Advanced observability platform | DEFERRED | Console logs plus host rotation and the two probes are sufficient for RC1; a metrics/trace platform is a later phase |
| 11.9 | Browser automation for layout verification | DEFERRED | No browser automation available; the layout contract is verified structurally |

## Deployment

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 12.1 | Ordered deployment procedure | PASS | `PRODUCTION_DEPLOYMENT.md` §1-§16 |
| 12.2 | Rollback considerations | PASS | §15: configuration, application, then database as a last resort |
| 12.3 | No real credential in any document | PASS | Placeholders and variable names only |
| 12.4 | No real deployment performed | NOT APPLICABLE | This phase produces a local release candidate |
| 12.5 | Git tag not created | PASS | Not created, as instructed |
| 12.6 | Application server configured on a target host | MANUAL | Owner: deploying engineer |
| 12.7 | Reverse proxy and TLS configured | MANUAL | Owner: deploying engineer |
| 12.8 | Initial administrator created | MANUAL | Owner: deploying engineer |
| 12.9 | Installation readiness verified on target | MANUAL | `check_installation_readiness` must pass on the target |
| 12.10 | Smoke tests on target | MANUAL | `PRODUCTION_DEPLOYMENT.md` §12 |
| 12.11 | Backup verified on target | MANUAL | Off-host copy confirmed |

## Monitoring

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 13.1 | Liveness probe | PASS | `/health/` — no database, no disclosure, safe methods only |
| 13.2 | Readiness probe | PASS | `/ready/` — bounded read in a transaction-local timeout, 503 when not ready |
| 13.3 | Probes unauthenticated and cache-free | PASS | `@require_safe`, `@never_cache`; proxy must not log their bodies |
| 13.4 | Production logging configured | PASS | Console only; `django.request` ERROR, `django.security` WARNING, `django.db.backends` WARNING |
| 13.5 | No secret in logs | PASS | No file handler, no mail handler, `ADMINS` empty, no secret setting name reaches a log call |
| 13.6 | Log rotation responsibility documented | PASS | `OPERATIONS_RUNBOOK.md` §Logs |
| 13.7 | Alert thresholds defined | MANUAL | Owner: site operator; thresholds depend on the deployment |
| 13.8 | Metrics and tracing platform | DEFERRED | Not required for RC1 |

## Dependencies

| # | Item | Status | Evidence |
| --- | --- | --- | --- |
| 14.1 | Direct dependencies pinned exactly | PASS | `Django==5.2.17`, `psycopg[binary]==3.3.6`, `python-dotenv==1.2.3` |
| 14.2 | Installed versions match the pins | PASS | All three match; `pip check` reports no broken requirements |
| 14.3 | No development dependency in production | PASS | `requirements/production.txt` includes only `base.txt` |
| 14.4 | Python compatibility | PASS | Python 3.14.4; Django 5.2 declares 3.10-3.14 |
| 14.5 | PostgreSQL compatibility | PASS | PostgreSQL 18.6; psycopg 3.3.6 |
| 14.6 | Broad major-version upgrades during hardening | NOT APPLICABLE | None performed |
| 14.7 | Transitive dependency pinning | DEFERRED | `asgiref`, `sqlparse`, `tzdata` are resolved by pip and documented as such in `DEVELOPMENT.md`; pinning them exactly is a later hardening step |

---

## Release blocker 1 — reporting page query budget exceeded (pre-existing)

### Phase 5D finding (original)

| # | Detail |
| --- | --- |
| Symptom | `apps.reporting.tests.test_reports.ReportingTests.test_representative_pages_have_bounded_queries` reports `58 not less than or equal to 24` for the operational `cases` page. `tests.test_phase3d_audit.CommercialReconciliationAudit.test_performance_all_representative_pages_and_streaming` reports `38 not less than or equal to 24` for the same page |
| Reproduces without Phase 5D | **Yes.** Both failures reproduce identically on the clean Phase 5C baseline `1a952d0` with every Phase 5D change stashed. This is not a Phase 5D regression |
| Introduced by | Phase 4D, when the shared shell was extended to `/reports/`, `/sla/`, `/communications/` and `/front-desk/`. The Phase 3D budgets were authored before the shell existed and were never re-baselined. The first full regression run in this release checkpoint surfaced it |

### Phase 5D.1 investigation (measured, not estimated)

Query attribution for `GET /reports/operational/?table=cases`, budget 24, captured
with `CaptureQueriesContext` and attributed by call site:

| Actor | Total | Shell authorization (`apps/operations/context.py` → `q.capable()`) | Page authorization (`apps/reporting/scope.py`) | Page non-authorization |
| --- | --- | --- | --- | --- |
| Minimal scoped reader (reporting test) | **55** | 38 | 12 | 2 |
| Superuser (Phase 3D test) | **37** | 20 | 12 | 2 |

Exact mechanism, confirmed by instrumenting `authorized_queryset`:

1. The navigation shell resolves **21 distinct capabilities per request**
   (`service.view_servicecase`, `devices.view_device`, `inventory.view_stock`, the
   five reporting capabilities, and so on). They are **21 distinct keys**, so the
   shell's own per-request `(permission, company_only)` memo has nothing to
   absorb — the cost is breadth, not repetition.
2. `apps/operations/queries.py:capable()` answers one boolean with **two**
   authoritative probes: `companies(actor, permission).exists()` and, when the
   persona is not company-scoped, `centers(actor, permission).exists()`. That is
   the 38-versus-20 difference between the two actors above.
3. The page separately evaluates `reporting.view_operational_dashboard` against
   `ServiceCenter` **11 times** through `apps/reporting/scope.py:centers()`, each
   embedding an identical authorization subquery.

### Phase 5D.1 outcome: no remediation applied, blocker stands

Two full audits were run, both deliberately and neither repeated:

| Run | Tests | Runtime | Failures |
| --- | --- | --- | --- |
| First (Phase 5D) | 2,352 | 4,599s | 3 — one Phase 5C ledger assertion since fixed, plus the two budget tests |
| **Second (Phase 5D.1)** | **2,378** | **6,939s** | **2** — the two budget tests, unchanged at 58 and 38 |

The second run was justified because Phase 5D.1 was chartered specifically against
those two failures and introduced 26 new tests that needed full-suite
verification. It confirmed no unrelated failure exists: 2,376 of 2,378 pass, and
the discovered count rose by exactly the 26 new tests.

The arithmetic floor cannot meet the budget without the changes this phase is
forbidden to make. With **zero** shell authorization queries the page alone still
costs **14** (12 authorization + 2 report queries). The shell must therefore fit
21 distinct capability decisions into **≤10 round trips**.

| Candidate optimization | Effect on the 55-query reader | Effect on the 37-query superuser | Verdict |
| --- | --- | --- | --- |
| Memoize `capable()` request-scoped | 0 | 0 | No repeated keys exist in the shell |
| Merge the two probes into one UNION-existence query built from the authoritative querysets | −17 → 38 | 0 (already short-circuits at one probe) | Outcome-preserving but still 38 / 37 |
| Memoize the page's 11 identical `scope.centers()` evaluations | −10 → 28 | −10 → 27 | Still above 24; §7 forbids over-optimizing unrelated page queries |
| All of the above combined | **28** | **27** | **Still above 24** |
| Batch 21 permissions into ~3 queries | meets budget | meets budget | **Forbidden** — re-derives `ADAPTERS[...].valid_target/containment`, the superuser branch and the `known_permission` branch outside `authorized_queryset`, i.e. duplicates authoritative authorization logic |

Per the Phase 5D.1 stop conditions, the last row alone is sufficient to stop.
Raising the 24 budgets, weakening or removing either frozen test, weakening
authorization, adding a process-level permission cache, and altering historical
migrations were all rejected.

| # | Assessment |
| --- | --- |
| Authorization layer changed | **None.** No production file was modified in Phase 5D.1 |
| Cache/snapshot lifetime | None introduced. The only memoization that exists is the per-request `(permission, company_only)` dict owned by `apps.operations.context.shell`, already covered by `tests/test_shell_capabilities.py` |
| Cache-key semantics | Not applicable; no new cache. The existing memo is keyed by the complete input set of `capable()` — `(actor, permission, company_only)` — which is exhaustive |
| Is it a scaling defect? | **No.** The companion test `test_diagnosis_engineer_invoice_outstanding_query_counts_do_not_grow` passes. The cost is fixed per request and bounded by navigation size, not by record count |
| Is it a correctness or security defect? | **No.** Every page returns correct, authorized data. Authorization is not weakened; more queries are strictly more conservative |
| Severity | Performance budget breach on read-only report pages. No data, authorization, integrity or availability impact |
| Regression guards added | `tests/test_shell_capabilities.py`, 26 tests: capability result equals the authoritative querysets; company-only capability never satisfied by a center scope; unknown and malformed permission identifiers denied; permitted navigation visible; denied navigation hidden; company scope does not unlock center-only navigation; cross-company, cross-center (sibling center in the same region) and cross-department isolation; deactivating a center removes the capability immediately; two personas never share a result; the shell memo is per-request, per-user and never reused; the shell resolves no capability twice per request; `authorized_queryset` still fails closed for combinators |
| Frozen budget tests | **Unchanged.** `git diff --stat` on `apps/reporting/tests/test_reports.py` and `tests/test_phase3d_audit.py` is empty; both still report 58 and 38 |
| Required before RC1 is declared READY | One of: (a) a separately reviewed change that resolves shell capabilities without duplicating authorization semantics, carrying its own authorization audit and an updated performance budget; (b) an explicit, approved re-baseline of the Phase 3D budgets together with a documented shell performance budget; or (c) a recorded operator decision to accept the current per-request cost for RC1, naming these two test IDs |

### Phase 5D.2 — authoritative bulk capability resolution (appended; history preserved)

**What was built.** An authoritative bulk capability API inside the existing
authorization layer, not in the shell:

- `apps/access/authorization.py::authorized_capabilities(user, permissions, company_only=…)`
  answers many capability questions in a bounded number of queries, and
  `authorized_capability_map(…)` answers both scope modes in one resolution.
- `apps/access/scopes.py::assignment_reaches(model)` is the clause-for-clause
  inverse of `ADAPTERS[model].containment`. `containment` filters an assignment
  while the target supplies the `OuterRef` columns; the bulk resolver needs the
  quantifier order reversed so the permission can be a row rather than a query
  parameter.
- `apps/operations/queries.py::capabilities()` / `capability_map()` delegate to it.
  `capable()` and `authorized_queryset()` are untouched.
- `apps/operations/context.py` now *declares* its capability questions, submits
  them once, and renders navigation from the returned decisions. It contains no
  RBAC logic.
- `apps/configuration/navigation.py` declares `CONFIGURATION_CAPABILITIES` so the
  shell and the configuration page cannot drift.

**Semantic-equivalence evidence.** `tests/test_authorization_bulk.py`, 22 tests.
The equivalence matrix compares bulk against `capable()` across 22 actor/scope
combinations (company, service center, sibling center in the same region, region
only, department, other department, other company, superuser, superuser with an
assignment) × three permission sets (full, sample, empty) × both scope modes —
more than 2,000 pinned comparisons — plus the containment mirror asserted against
`authorized_queryset` for every assignment and every Company, Region,
ServiceCenter and Department target. Mixed allowed/denied sets, duplicate
requests, unknown and malformed identifiers, inactive and anonymous actors, and
role/role-assignment/centre deactivation are all covered. During development an
equivalent standalone run performed **18,000** comparisons with zero mismatches.

**Before/after query counts** for `GET /reports/operational/?table=cases`:

| Actor | Before | After | Budget |
| --- | --- | --- | --- |
| Minimal scoped reader | 55 (shell 38) | **24 (shell 4)** | 24 |
| Superuser | 37 (shell 20) | **24 (shell 4)** | 24 |

Shell authorization queries fell from 38/20 to **4**, independent of the number of
navigation capabilities. **The reported blocker — `('cases', 58)` — is resolved**;
the operational `cases` and `engineer_queue` reports now measure exactly 24 against
a 24 budget.

**Why the two tests still fail.** They iterate further than the reported
blocker, and the loop aborts at the first failing assertion, so the assertions
after `cases` were never reached in any earlier run. Measured shell/page split:

| Report | total | shell | page alone | budget | total ok | page alone ok |
| --- | --- | --- | --- | --- | --- | --- |
| operational / cases | 24 | 4 | 20 | 24 | **yes** | yes |
| operational / engineer_queue | 24 | 4 | 20 | 24 | **yes** | yes |
| service / complaints_complaint | 15 | 4 | **11** | 10 | no | **no** |
| service / diagnoses | 16 | 4 | **12** | 10 | no | **no** |
| inventory / positions | 12 | 4 | 8 | 8 | no | yes (needs a zero-query shell) |
| commercial / invoices | 13 | 4 | 9 | 10 | no | yes (needs a ≤1-query shell) |
| commercial / outstanding | 13 | 4 | 9 | 10 | no | yes (needs a ≤1-query shell) |

`service/complaints_complaint` and `service/diagnoses` perform **11 and 12
queries of their own**, exceeding their frozen 10-query budget even with a
zero-query navigation shell. The 8/10 budgets for the inventory and commercial
sections leave room for at most 0–1 shell queries while rendering an
authorization-checked navigation tree, which would require removing navigation
permission checks. Clearing these requires either optimising the frozen Phase 3D
reporting builders — explicitly out of scope — or re-baselining the frozen
budgets. Neither was done.

**A reporting-scope optimisation was attempted and reverted.**
`apps/reporting/scope.py::centers()` is called ~10 times per render with the same
`(user, permission, export)`. The premise for §8's narrow optimisation is
*repeated identical authoritative work*. Instrumenting the real fixtures showed
the repeated calls are lazy queryset construction: the authorization predicate is
embedded in the consumers' own aggregate queries and costs no separate round
trip. Materialising the authorized id set once made `cases` **worse** (24 → 26).
The premise is therefore false, the change was reverted, and
`apps/reporting/scope.py` is unchanged.

**Final full audit: NOT run.** Per the Phase 5D.2 rule, the full audit runs only
if both original query-budget tests pass unchanged. They do not, so no third full
audit was executed.

### Phase 5D.3 — targeted report-page round-trip reduction (appended; history preserved)

**Profiling first.** Every query on every representative page was captured and
attributed to its call site before any edit, for both the minimal scoped reader
(reporting test) and the superuser (Phase 3D test). The profile corrected the
Phase 5D.2 "page alone" figures (11/12): they had counted one template-triggered
query and two shell `has_perm` queries as report cost. **No report page has an
N+1, per-row or duplicated builder query.** Every excess is a fixed-cost
round trip:

| Bucket (scoped reader) | complaints | diagnoses | positions | quotations / invoices / outstanding | payments |
| --- | --- | --- | --- | --- | --- |
| Framework: session + user | 2 | 2 | 2 | 2 | 2 |
| Shell: bulk capability map (Phase 5D.2) | 4 | 4 | 4 | 4 | 4 |
| Shell: `configuration.navigation.visible()` → `user.has_perm()` cold permission cache | 2 | 2 | 2 | 2 | 2 |
| Page access probe `accessible()` | 1 | 1 | 1 | 1 | 1 |
| Paginator `COUNT` | 1 | 1 | 1 | 1 | 1 |
| Page rows | 0 (empty) | 1 | 1 | 1 | 0 (empty) |
| Service card `performed_actions().count()` | 1 | 1 | – | – | – |
| `service_rates()`: three separate `.aggregate()` | 3 | 3 | – | – | – |
| Quotation-approval turnaround | – | – | – | 1 | 1 |
| **Template** `{% report_export_allowed %}` → second `accessible(export=True)` | 1 | 1 | 1 | 1 | 1 |
| **Total / budget** | **15 / 10** | **16 / 10** | **12 / 8** | **13 / 10** | **12 / 10** |

The superuser is 2 lower on each page (`visible()` short-circuits on
`system_administrator` before `has_perm`).

**Proven Phase 5D.2 regression found and fixed.** `apps/configuration/navigation.py`
declared and checked `sla.manage_slopolicy`. No such permission exists; the
model, migration, services, queries, demo data and tests all use
`sla.manage_slapolicy`. The bulk resolver correctly denies unknown identifiers, so
since Phase 5D.2 an SLA-policy manager lost the shell's *SLA policies* link and the
*Setup* entry. It failed closed (no scope widening) but was a functional
regression no Phase 5D.2 test caught. Fixed in all three places, with a guard
asserting every shell-declared capability is a real permission.

**Remediation — four narrow changes, no builder semantics touched:**

| # | Change | Where | Saves |
| --- | --- | --- | --- |
| A | In `visible()`, evaluate the resolved capabilities before `user.has_perm()` (`is_staff` stays first). Pure boolean reordering; verified against the previous expression for all 512 input combinations | `apps/configuration/navigation.py` | 2 (staff, non-admin) |
| B | One `access()` probe answers view **and** export access: four `EXISTS(...)` built by `Query.exists()` — the exact SQL `QuerySet.exists()` runs — over the unchanged authoritative `scope.centers()` / `scope.companies()` querysets, in one statement. The view passes `can_export` to the template; the template tag that re-probed is removed | `apps/reporting/views.py`, `apps/reporting/query.py`, `report.html`, `workspace_tags.py` | 1 |
| C | The service card and the three ratio aggregates are compiled unchanged as single-row aggregates (`totals()` = `.aggregate()` without executing; no GROUP BY, so exactly one row each) and read together via one cross-joined `SELECT` | `apps/reporting/service_analytics.py` (`rate_totals`, `rates_from`, `service_rates`), `apps/reporting/query.py` | 3 |
| D | Page rows and total in one query: `COUNT(*) OVER ()` is evaluated after WHERE/GROUP BY and before LIMIT, so it equals `rows.count()`; an empty page beyond 1 is always past the last page (total ≤ offset), so it 404s exactly as before | `apps/reporting/views.py::page_of` | 1 |

A first version of D skipped the COUNT only when the page was incomplete. The
frozen `BoundaryTests.test_stable_pagination_and_constant_dashboard_and_complaint_queries`
correctly rejected it (20 → 21 once rows exceed one page): it was data-size
dependent. It was replaced by the size-independent window total before any further
verification; the frozen test was not touched.

**Not done, deliberately:** no scope ID materialisation (the Phase 5D.2 experiment
is not repeated); no change to `authorized_queryset`, the bulk resolver, or any
report builder's rows; no new cache of any lifetime; no budget, test or migration
change.

**Before/after query counts** (frozen budgets unchanged):

| Page | Reader before | Reader after | Superuser after | Budget |
| --- | --- | --- | --- | --- |
| operational / cases | 24 | **20** | 20 | 24 |
| operational / engineer_queue | 24 | **20** | 20 | 24 |
| service / complaints_complaint | 15 | **9** | 9 | 10 |
| service / diagnoses | 16 | **9** | 9 | 10 |
| service / root_causes, co-occurrence, diagnosis_records | 16 | **9** | 9 | 10 |
| inventory / positions | 12 | **8** | 8 | 8 |
| commercial / quotations, invoices, outstanding | 13 | **9** | 9 | 10 |
| commercial / payments | 12 | **9** | 9 | 10 |
| payment CSV incl. iteration | – | **4** | 4 | 8 |

Counts are now independent of data size (the frozen growth tests pass).
`inventory/positions` sits exactly at its budget: framework 2 + shell 4 + access 1
+ rows-with-total 1. That is its floor without touching shell authorization.

**Semantic-equivalence evidence.** `tests/test_reporting_round_trips.py`
(19 tests) compares every changed path with a verbatim copy of the code it
replaced:

- `access()` vs legacy `accessible()` view/export probes for 10 personas
  (center, center without export, sibling center, region, company, department,
  other company, service-only, no grant, superuser) × 5 sections;
- `service_rates()` vs legacy aggregates per persona × 5 date windows (including
  today-only, until-yesterday and from-tomorrow half-open boundaries) × 4 extra
  filters, plus the rework fixture's non-trivial ratios (100 / 0 / 50);
- full service pages (cards, ratios, rows, totals, export link) and one page per
  section vs legacy, for every persona;
- `page_of()` vs `Paginator.get_page()` for sizes 0, 1, 49, 50, 51, 100, 101 at
  every page including past the end, and for **every table of every builder**
  (more than 150 tables across three actors);
- `combined_totals()` / `combined_exists()` vs `.aggregate()` / `.exists()`,
  including empty sets and NULL sums;
- scope isolation (other company and sibling center see zero), export permission
  enforcement, and denial for users without the section permission.

**Focused verification:** 427 tests OK in 1,585s — all `apps.reporting`,
`tests.test_phase3d_audit`, `tests.test_reporting_round_trips`,
`tests.test_shell_capabilities` (26), `tests.test_authorization_bulk` (22),
`apps.configuration`, `apps.operations`, `apps.sla`, `apps.access`,
`tests.test_production_surface`. Both original budget tests pass unchanged.
`manage.py check`: no issues; `makemigrations --check`: no changes;
`git diff --check`: clean.

**Test-suite hygiene.** `tests/test_zz_probe.py` (untracked, Phase 5D.2-era) was
removed. It contained no assertions, only `print` diagnostics, was referenced
nowhere, monkeypatched `apps.operations.queries.authorized_capability_map` for the
whole test process at import time, and, by subclassing `ReportingTests`, re-ran
every inherited reporting test (including a frozen budget test) a second time.
It was diagnostic instrumentation, not regression coverage.

**Third full audit (Phase 5D.3 RC verification attempt): FAILED — RC1 remained BLOCKED.**

| Run | Tests | Test runtime | Audit elapsed | Failures | Errors |
| --- | --- | --- | --- | --- | --- |
| Third (Phase 5D.3) | 2,416 | 5,381.699s | 5,424.478s | **1** | **9** |

The Phase 5D.3 representative query budgets were green in that run (cases 20,
engineer workload 20, complaints 9, diagnosis/root-cause reports 9, positions 8,
invoices 9, outstanding 9, payment CSV 4). It exposed two new blockers, both
introduced by Phase 5D.3 change D and neither caught by the focused gate:

*Blocker A — malformed/extreme `page` input reached PostgreSQL.*
`tests.test_phase3d_security_audit.ReportingSecurityAudit.test_bad_inputs_fail_without_traceback_or_sql`
requests `page=` followed by 4,000 nines. The old view called
`Paginator.get_page()` first; it counts, clamps to the last page, and the
page-number mismatch returned 404 before any OFFSET was built. Change D reads the
page rows **first**, so `(page - 1) × 50` was sent as the OFFSET and PostgreSQL
raised `NumericValueOutOfRange: bigint out of range` → `DataError`. **That is the
one primary failure.** It aborted the test's transaction, and every later
statement in the test raised `InFailedSqlTransaction`. Those are the cascading
**errors**, not independent defects. The focused gate had not included
`tests.test_phase3d_security_audit`. It now does.

*Blocker B — `21 != 20` query stability.*
`apps.reporting.tests.test_boundaries.BoundaryTests.test_stable_pagination_and_constant_dashboard_and_complaint_queries`.
The audit ran against the working tree while the **intermediate** form of D was
present (skip COUNT only on an incomplete page). The rejection is recorded above.
Profiling the test's exact request sequence with that version patched back in
reproduces it deterministically: `/reports/?table=cases` goes 20 → **21** after
51 cases are added, and the single extra statement is the paginator's
`SELECT COUNT(*) … service_servicecase`. It is not test-order dependent. With
the window total now in the tree, the same sequence measures 20 → 20 (service
page 9 → 9).

A further full run started at 17:14 on the window-total tree was **stopped
deliberately at about 47 minutes**. Its log already showed the same Blocker A
`bigint out of range` cascade. Finishing it could only re-confirm a known
failure, so it does not count as a verification run.

**Remediation.**

- Blocker A: `page_of()` now raises 404 before any query when
  `(page - 1) × 50 > 2**63 - 1` (`MAX_OFFSET`, PostgreSQL's bigint OFFSET bound).
  No table can hold enough rows for such a page to exist, so this is exactly the
  old "past the last page → 404" outcome. Valid pages, the zero/negative → 400
  contract (unchanged, still validated earlier in the view) and stable
  pagination are untouched. Regression test
  `test_extreme_page_is_404_and_never_reaches_postgresql_as_an_offset` proves
  zero queries for pages past the bound (including 2**63, 2**64 and 4,000 nines).
  It also proves one harmless empty read for the last in-range page, and that
  the connection remains usable (no aborted transaction). The frozen security
  test passes unmodified.
- Blocker B: no further change needed. It was already removed by the
  size-independent `COUNT(*) OVER ()` form of D, which the profile above
  verifies.

**Focused gate before the post-fix audit:** 439 tests OK in 1,183s. It covers
all of the earlier focused set plus `tests.test_phase3d_security_audit`, which is now
permanently included. Every test module that requests `/reports/` or uses the
shell is covered. `manage.py check`, `makemigrations --check` and
`git diff --check` were clean.

**Fourth full audit (Phase 5D.3 post-fix RC verification): PASSED.** Run exactly
once, after the focused gate.

| Run | Tests | Test runtime | Audit elapsed | Failures | Errors |
| --- | --- | --- | --- | --- | --- |
| Fourth (Phase 5D.3 post-fix) | **2,419** | 5,314.067s | 5,367.967s | **0** | **0** |
| Operator post-fix confirmation | **2,419** | 5,415.298s | 5,462.410s | **0** | **0** |

Audit checks: Django system check, migration drift, unapplied migrations, git
whitespace, `.env` not tracked and full regression all **PASS**; `AUDIT CHECKS
PASSED`. The test count rose by exactly 3 from the third run: the three tests
`tests/test_reporting_round_trips.py` gained between the runs (16 → 19). The
in-audit representative counts match the focused measurements above (cases 20,
engineer workload 20, service pages 9, positions 8, commercial pages 9, payment
CSV 4).

The operator's separate post-fix confirmation run of the same tree reported
**2,419 tests, 0 failures, 0 errors**, 5,415.298s test runtime, 5,462.410s audit
elapsed, `PASS: Full regression`, `AUDIT CHECKS PASSED`. The two runs differ only
in wall-clock time. Every code and test file was last modified before either run
started. The only later edits are documentation and docstrings made during the
Phase 5D.4 freeze review (see below), none of which changes executable code.

## Summary

Counted directly from the item rows above. The previous summary table
(PASS 81 / BLOCKED 1 / MANUAL 7 / DEFERRED 5 / NOT APPLICABLE 8) did not match the
rows it summarised. Recounting the rows as they stood before Phase 5D.3 gives
102 / 2 / 7 / 6 / 10. Phase 5D.3 added 10.10 (PASS), 10.11 (BLOCKED, historical)
and 10.12 (PASS).

| Status | Count |
| --- | --- |
| PASS | 104 |
| BLOCKED (current) | **0** |
| BLOCKED (historical, superseded: 10.4, 10.9, 10.11) | 3 |
| MANUAL | 7 |
| DEFERRED | 6 |
| NOT APPLICABLE | 10 |

**Release blocker 1 is resolved.** History, preserved above:

1. First Phase 5D full audit: 3 failures.
2. Phase 5D ledger-assertion correction.
3. Second full audit: 2 failures, the two frozen budget tests.
4. Phase 5D.1: measured the shell, then stopped correctly.
5. Phase 5D.2: authoritative bulk capability resolver; shell 38/20 → 4 queries;
   `cases` within budget. That exposed the masked report-page budgets.
6. Phase 5D.3: fixed-cost round trips attributed and removed. Found and fixed a
   Phase 5D.2 permission-identifier regression. Removed the diagnostic probe
   module. The third full audit failed (1 failure, 9 errors), exposing an
   extreme-page OFFSET defect and a size-dependent count, both introduced by
   Phase 5D.3 change D and both fixed.
7. Fourth full audit: **2,419 tests, 0 failures, 0 errors**, confirmed by the
   operator's separate run with the same result.
8. Phase 5D.4 freeze review: documentation and docstring corrections only (see
   below). No executable code was changed, no migration was created, and the full
   audit was not rerun.

Both original frozen tests —
`apps.reporting.tests.test_reports.ReportingTests.test_representative_pages_have_bounded_queries`
and
`tests.test_phase3d_audit.CommercialReconciliationAudit.test_performance_all_representative_pages_and_streaming` —
pass with their budgets, assertions and files unchanged.

The seven `MANUAL` items are deployment-host actions by design and have named
owners. The `DEFERRED` items are known, accepted limitations covering five
topics: no SMS vendor, no provider delivery-status reconciliation, no advanced
observability/metrics platform (rows 11.8 and 13.8), no browser automation, and
exact pinning of transitive dependencies.

## Phase 5D.4 — RC1 freeze review

Review/freeze only. No feature, behaviour, budget, test-assertion or migration
change. The full audit was not rerun.

| # | Finding | Action |
| --- | --- | --- |
| F.1 | `tests/test_zz_probe.py` — untracked Phase 5D.2-era diagnostic probe (no assertions, `print` output only, process-wide monkeypatch, re-ran inherited reporting tests) | Already removed in Phase 5D.3 and confirmed absent. Not regression coverage |
| F.2 | `tests/test_shell_capabilities.py` module and method docstrings still stated that the two frozen Phase 3D tests fail and that the budgets "cannot" be met | Docstrings corrected. The module's syntax tree with docstrings stripped is byte-identical before and after, so no test behaviour changed |
| F.3 | `docs/ARCHITECTURE.md` — missing opening backtick before `/ready/`; checklist row 4.13 had an unescaped `\|` that split its table cell | Both fixed |
| F.4 | `.env.example` and `config/views.py` have no trailing newline | Cosmetic. Left as is during the freeze; `git diff --check` passes |
| F.5 | Unused imports: `ServiceEngineerAssignment` and `durations` in `apps/reporting/service_analytics.py` (both present at the Phase 5C baseline), and `az` in `tests/test_shell_capabilities.py` | Pre-existing or harmless. Left as is during the freeze |
| F.6 | Red-flag scan of the whole RC1 diff (debug prints, probes, credentials, skips, `expectedFailure`, `\|safe`, `csrf_exempt`, raw SQL) | Clean. Hits are operator runbook commands, a test-only ephemeral key used to exercise the production settings guard, and a fresh superuser re-read in the bulk resolver |
