Add pack rules API and gap count
# C1 — API: pack rules endpoint + matrix gap_count
Part of epic "Simplify and improve Compliance UI clarity". This child adds the two **additive** backend pieces the redesign reads, plus the web client types. No semantic change.
## Scope (Architecture §2.1, §2.2, §2.4)
**1. `GET /api/compliance/packs/{pack_id}/rules`** in `apps/api/app/routes/compliance.py`, beside `get_rule` (~line 775):
- Guard: `@feature("compliance", "view")` plus `_org_id`, the same as `get_rule`. Pack resolution goes through `packs.pack_for` (own org pack, else managed). An unknown pack or another org's pack → **404 with detail `pack`**, the exact shape `_pack` raises (`compliance.py:114-125`), so `isPackNotFound` (`api.ts`) recognises it.
- Route parameter: `pack_id: str`. A value that does not parse as a UUID → **404 `pack`** (never FastAPI's 422).
- Rules come from `packs.rules_for(session, org_id, pack.id)` (`apps/api/app/governance/packs.py:177`, `version=None` → the org's applied release for a managed pack). Change rules are **included**. Order is `rules_for`'s (level, key).
- `RuleRecord` holds non-null `pillar`/`level` even for change rules: the endpoint **sends `null` for both when `subject_kind == "change"`**.
- Response 200: `{ "pack": <exactly _pack_out(pack)>, "rules": [ { "id": _rule_ref(rule) (row uuid for an org rule, "<pack uuid>:<key>" for a managed one — what GET /rules/{rule_id} accepts), "key", "name", "subject_kind": "structural|change", "change_kind": check.kind for a change rule else null, "pillar": str|null, "level": int|null, "mode": "observe|advise|nudge|gate|lock", "enabled": bool } ] }`.
**2. Matrix row `gap_count`**: `GET /api/compliance/matrix` each row gains `"gap_count": len(gaps)` — the same `gaps` list `top_gap` is taken from (`compliance.py:~502-513`). Always sent, scored or not. No new computation.
**3. Web client** in `apps/web/src/pages/compliance/api.ts`: `export type PackRule`, `export type PackRules = { pack: PackOut; rules: PackRule[] }`, `export const packRulesKey = (packId: string) => [...COMPLIANCE_KEY, 'pack-rules', packId]`, `export function getPackRules(packId: string): Promise<PackRules>`; `MatrixRow.gap_count?: number`; `export function modeLabel(mode: string): string` = `MODE_LABEL[mode] ?? mode` next to `MODE_LABEL`.
**Tests** in `apps/api/tests/test_compliance_api.py`: (a) a managed pack lists every rule of its applied release, change rules included, and its count equals the TOML's rules (use an org on a non-head release where available, else document the head); (b) an org (forked) pack lists its `gov_rules`, change rules included, with `pillar`/`level` null for change rules; (c) another org's pack → 404 `pack`, and a non-UUID id → 404 `pack`; (d) for every row, scored or not, `gap_count` equals the number of failing cells (the `gaps` list `top_gap` comes from).
## Original request (verbatim, epic Task 10032)
> Go over Compliance process with oam-demo project in prod as an example. The Compliance UI seems to be too busy, very confusing, not clear where user actually sets up rules, etc. Clean up and make more straight and intuitive. Remove repetitions and stating obvious facts
## Settled requirements (PRD, verbatim)
- **R1: One place to set up rules (A3, A4, A5; D1, D2, D6).** `/compliance` is a tabbed page: Repos · Rules · Guard activity (only with `guard_enabled`) · Settings. The tab is in the URL. Rules lists every rule of the selected pack (default: the active pack), both scan and change rules. It has filters for at least pillar, level, mode and on/off. Each row opens that rule's editor (`/compliance/rules/:id` or its successor). **Add rule** sits on the Rules tab for an org pack. A ready-made pack shows its rules read-only, with the Fork/Customize action and one line of why beside the list.
- **R5: Consistent words (A3).** Mode values use their labels (Observe/Advise/Nudge/Gate/Lock) everywhere. Every label follows `docs/standards/ui-copy-standards.md`.
- (R2–R4, R6, R7 bind the UI children C2–C5; this child supplies their data.)
Architect assumption S1 (PRD): "Full redesign" changes the UI only. The scan engine, scoring, levels, rule semantics and stored data are unchanged. An API may be **added** where a new surface needs it (e.g. listing a pack's rules: today no `GET` returns a pack's full rule list, and `AddRulePage` borrows `getMatrix`).
Full design docs: the epic's PRD and Architecture artifacts (Tasks 10032 / 10033); §-numbers above refer to the Architecture document.
Credentials & reach (Architect §7, verbatim):
- AC1–AC17 (dev console UI), guard **on** states — `me/jaah/debug/main/operator-login` (owner/admin in several dev orgs). Environment: dev control plane via its public ALB **`https://app.dev.jaah.ai`** (`docs/architecture.md:185`). Target tenant: **Org B** — **verified**: `credential_get` succeeded. A sign-in to `app.dev.jaah.ai` returned 200. Switching to Org B returned 200, its session reads `compliance_enabled` and `guard_enabled` true, and `GET /api/compliance/setup` returned 200 (10 projects, 3 packs). The internal `dev.jaah.ai` ALB timed out from this node (tailnet-only), so oracles use `app.dev.jaah.ai`.
- AC18 (`./dc check quick`, unit + e2e suites) — Nothing beyond the worktree — **verified** (worktree only)
Out of scope:
- Do NOT touch `/compliance/verify` (`apps/web/src/pages/ComplianceVerifyPage.tsx`), the task manifest (`apps/web/src/components/tasks/ComplianceManifest.tsx`), org compliance signing (`apps/web/src/pages/settings/complianceSigning/`), `apps/web/src/pages/orgs/OrgSettingsTab.tsx`, `apps/web/src/pages/customers/ComplianceDeleteDialog.tsx` or the plan feature (`apps/web/src/pages/products/plan/PlanFields.tsx`).
- Do NOT change scan/score semantics: `apps/api/app/routes/compliance.py` matrix/repo/cell computation, levels, the auditor runner, the daily-scan cron behaviour, Guard enforcement in `apps/jaahd/guards/`.
- Do NOT change permissions: `compliance:view` / `compliance:edit` and the `RequireCompliance` / plan gating stay as they are.
- Do NOT add a second sidebar entry for Compliance, and do NOT add a feature flag for the new UI.
- Do NOT remove any capability silently; a removal is allowed only if listed in Architecture §6 (which drops none).
- No data migration: stored packs, rules, waivers, facts and scans are untouched.
- Do NOT remove the matrix `observed` API field (deferred follow-up).
- No UI page changes here (C2–C5 own them).
Done means:
- AC1 (verbatim): "The Rules tab lists every rule of the active pack (scan + change), and its count matches the pack's rule count" — this child owns the data half: `GET /api/compliance/packs/<active pack id>/rules` on dev (Org B, operator-login) returns a `rules` array whose length equals the pack's rule count (managed: the applied release's TOML rule count; org pack: its `gov_rules` count), change rules included; tests (a)/(b) pin it.
- Endpoint contract: another org's pack and a non-UUID id both answer 404 with detail `pack` (never 422) — test (c).
- `gap_count`: every matrix row carries `gap_count` equal to its failing-cell count — test (d); `GET /api/compliance/matrix` on dev shows the field on every row.
- `modeLabel`, `getPackRules`, `packRulesKey`, `PackRule(s)` and `MatrixRow.gap_count` are exported from `pages/compliance/api.ts` and type-check.
- AC18 (api part): `./dc check quick` passes; `./dc test tests/test_compliance_api.py` passes.
Outcome
Added GET /api/compliance/packs/{id}/rules, matrix gap_count, and web client types; quick gate + test_compliance_api (30) green; PR #6916 merged (f4ff642). Dev oracle unverified: its deploy was superseded by a later queued run.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)