Rebuild rule editor and Add rule
# C5 — Rule page editor and Add rule
Part of epic "Simplify and improve Compliance UI clarity". Rebuilds `/compliance/rules/:id` and `/compliance/packs/:packId/rules/new`. Depends on C1 (`getPackRules`, `modeLabel`). (C2 adds both routes to `LIST_DETAIL_ROUTES` beside the Rules tab; URLs are unchanged here.)
## Scope (Architecture §3.8, §3.9)
Files: `RulePage.tsx`, `AddRulePage.tsx`, `ScopeBuilder.tsx` as needed, `WaiverList.tsx` only for `hideWhenEmpty` if chosen; tests `RulePage.test.tsx`, `AddRulePage.test.tsx`, e2e rule-editor/scope/add-rule cases in `apps/web/e2e/compliance.spec.ts`.
**Rule page:**
- **Breadcrumb:** `Compliance › Rules` (→ `/compliance?tab=rules&pack=<pack id>`).
- **Title:** the rule name. Subtitle: `Pillar name · L1 Level name` for a scan rule, or `Change rule · <kind>` for a change rule. Pack, version and key leave the subtitle.
- **Header action:** ready-made pack + `compliance:edit` → **Customize**, linking to `/compliance/settings?fork=<id>` (as today, `RulePage.tsx:~264`): it works before C2 lands, and after C2 its redirect lands on `/compliance?tab=rules&pack=<id>&customize=1`.
- **Org rule + `compliance:edit`:** one form inside `ui/detail/DetailPage` with `actionBar` (`ui/FormActionBar`, Save disabled until dirty, `UnsavedChangesDialog` on leave). Controls: **On** switch; **Mode** select (labels + hints as today); **Fix guidance** (guard only; required at gate/lock, same rules as today); **Applies to** (`ScopeBuilder`; when the scope changed, Save also needs a preview, as today). Save sends **one** `PATCH /rules/{id}` with the changed fields only (replaces per-control autosave). Field problems sit beside their control; on a refusal (one 422 with `problems`) every draft stays.
- **Read-only:** a `KeyValueList` with `Intent` and `Why it matters` (only when set), `Mode` (`modeLabel`, plus ` · Off` when off), `Fix guidance` (when set), `Applies to` (**only when not every repo**), `Applies when` (when set), `How it is checked`, `Oracles` and `Key` (monospace, last). The `Kind: Structural rule` row is dropped.
- **Sections:** `Failing repos (n)` ("Passes on every scanned repo you can see." when 0), each row the repo link (→ repo page) and an explicit **Evidence** link (→ cell pane); `Not scanned (n)` only when > 0; `Active waivers` only when any exist; Guard blocks only when > 0.
**Add rule:** breadcrumb `Compliance › Rules`, title `Add rule`, Save in `FormActionBar`, on success navigate to the new rule's page (existing behaviour). Pillars/levels come from `getPackRules(packId).pack` instead of `getMatrix` (`AddRulePage.tsx:~330`). Mode values via `modeLabel`.
Dropped/moved (Architecture §6, this child's rows): rule page "Kind: Structural rule", "Applies to: every repo" (read-only) and raw mode value removed; pack · version leave the rule subtitle (breadcrumb points to the Rules tab).
## 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.
- **R2: Less on screen (A2).** Each page has at most **3 visible header actions** at ≥1280px. The rest go in one overflow menu or move to the tab that owns them (e.g. Settings).
- **R3: No repetition, no obvious facts (A6, A7).** Every item F2–F10 in §4 is resolved: the repeated text is removed or said once, and the obvious label is dropped. The spec names how each one is resolved. Specifically, no rendered Compliance surface:
- shows a status badge that only restates the section it sits in (e.g. "Fail" under "Gaps");
- shows a field whose value is the default ("every repo", zero counts, "None") unless it is editable there;
- repeats the same sentence on more than one row of a list (a shared state is said once for the group);
- shows the same fact twice in one header (badge + sentence, or pack + version in the header *and* the subtitle).
- **R4: One name, one destination (A3).** A rule's name opens the same kind of page wherever it appears: the rule page. Evidence for a repo × rule is a separate, explicitly labelled action or link.
- **R5: Consistent words (A3).** Mode values use their labels (Observe/Advise/Nudge/Gate/Lock) everywhere. Every label follows `docs/standards/ui-copy-standards.md`.
- **R6: Dropped features are explicit (D5).** The architecture spec lists every removed feature with a one-line reason. Anything not listed stays reachable. No feature disappears silently.
- **R7: Available on dev (charter).** The redesigned UI is the only Compliance UI: no flag or toggle gates it. It is live on the dev control plane (`dev.jaah.ai`) for an operator whose org has Portfolio Compliance on.
Operator decisions: D2 keep explicit Fork (renamed **Customize**); D3 full redesign to the UI style guide; Architect Q1/Q2: **no capability is dropped**.
**Binding design principle:** a page shows at most one always-visible primary action; any other action goes into the page's ⋯ (`ui/OverflowMenu`), a row's ⋯, or appears only while its condition holds. Breadcrumb ends at the parent; the page title names the page; a breadcrumb never repeats the title. Every rendered mode goes through `modeLabel`.
Full design docs: the epic's PRD and Architecture artifacts (Tasks 10032 / 10033); §-numbers refer to the Architecture document; its §6 "Dropped / moved" table is binding.
States covered: an org with **no scan yet**, **a scan running**, **results present**, **a project not scannable**, a **ready-made** active pack and an **org (forked)** pack. A viewer is either **compliance:view only** or **compliance:edit**, and `guard_enabled` is **on** or **off**. Unless a criterion names a state, it holds in all of them. Desktop = 1440×900; phone = 390×844 (`docs/standards/ui-style-guide.md` § Checking a page).
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`.
- AC1–AC17, guard **off** states — Same credential. Target tenant: **TimeSaver2** — **verified**: the switch returned 200, `compliance_enabled` true, `guard_enabled` false, and `GET /api/compliance/setup` returned 200 (2 projects, 2 packs)
- AC6 (compliance:view-only viewer) — Same credential: the login is `owner` in Org B (admin covers non-admin). The coder adds a view-only member through the console's Users/Groups UI at verify time and signs in as it — **verified** (owner identity probed above; creating the member is a console step, not a missing credential)
- AC8 / AC16 data (≥2 not-scannable projects, fresh results) — Same credential, Org B set up through the console (projects with no node, a rescan) — **verified** (Org B reachable with 10 projects; data is arranged at verify)
- 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 change rule semantics or the `PATCH /rules/{id}` contract (`RulePatch`); do NOT build the tab shell, Rules tab, repo page or Repos tab (C2, C3, C4).
Done means:
- AC5 (save half): Add rule starts from Rules — With an org pack active, the new-rule page (`/compliance/packs/<id>/rules/new`) saves and returns to the new rule, and that rule appears in the Rules list — oracle here: after save the browser is on `/compliance/rules/<new id>` and `GET /api/compliance/packs/<id>/rules` (C1) returns the new rule (the Rules-tab half is C2's).
- Customize on a ready-made rule lands on a page offering Customize (today's Settings fork hint, or the Rules tab once C2 merged).
- AC6 (rule page): View-only viewers see no edit controls — As a compliance:view viewer, the rule page shows no Customize, toggle, mode select, scope builder or Save. Each that has a reason shows it once in plain words.
- AC7 (rule, add rule): ≤3 visible header actions — At 1440×900, on the rule page and Add rule, count the visible buttons/links in the page header (excluding the overflow ⋯). There are ≤3.
- AC9 (rule page): On the rule page: no "Applies to: every repo" unless it is an edit control; no "Kind: Structural rule"; the breadcrumb does not repeat the title.
- AC11 (destination): Every rule-name link in the app lands here; on this page each failing repo has an explicit **Evidence** link and the repo name opens the repo page.
- AC12: Mode labels — Search the rule page's and Add rule's rendered text: no lower-case raw mode token (`observe`, `advise`, `nudge`, `gate`, `lock`) stands as a value; the label is used.
- One Save: changing mode + scope + guidance and saving sends one PATCH; a nudge refusal with a dirty scope keeps every draft and shows the problem beside its control (unit test pins it); leaving with unsaved changes prompts.
- AC16 (rule page): Readable, not just smaller — At 1440×900 and 390×844 on the rule page: no rule or project name truncated to an ellipsis (names wrap); no horizontal page scroll. **Tightened:** attach 1440 and 390 screenshots of the rule page (org rule editor, ready-made read-only, change rule) and Add rule to the report; a form whose Save bar overlaps content or whose fields misalign fails.
- AC18: `./dc check quick` passes; `RulePage.test.tsx`, `AddRulePage.test.tsx` and the rule-editor/scope/add-rule cases of `apps/web/e2e/compliance.spec.ts` pass, updated to the new structure.
Outcome
Rule page rebuilt as one form (one PATCH, sticky Save, leave prompt, Customize, Evidence links) and Add rule on getPackRules with Save bar. Quick gate, 265 unit tests, 10 e2e green; 1440/390 visual check clean. PR #6929 merged (15b15425); its deploy run was superseded by the next develop push. Memory: none applied.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)