Commit 8a1c46f
fix(treatment-plan): cap linked-work lists and treatment plan body height
* fix(treatment-plan): cap linked-work lists and treatment plan body height
Long Tasks/Controls lists or AI-generated treatment plans were stretching
the right (Linked Work) and middle (Treatment plan) columns past the
Strategy column, leaving the row visually unbalanced and forcing the
whole page to scroll just to see the bottom controls.
- LinkedWork: each list (Tasks, Controls) now caps at max-h-80 with
internal overflow-y-auto. The progress bar / counts stay pinned.
- DescriptionEditor: markdown preview and the auto-growing textarea
share a TEXTAREA_MAX_PX (480) cap. Past the cap, internal scroll
takes over instead of pushing the row down.
Net effect: the three columns stay roughly aligned regardless of how
many tasks/controls are linked or how long the AI's plan runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(treatment-plan): paginate linked tasks and controls (4 per page) + seed fix
LinkedWork
- Each list (Tasks, Controls) now paginates at 4 items per page with
prev/next + "X–Y of N" controls. Replaces the prior height cap +
internal scroll. Long linked sets stay visually compact and the
Linked Work column no longer pushes past the Strategy / Treatment
plan columns regardless of list size.
- Pagination state resets defensively when the underlying list
shrinks past the current page (e.g. after an unlink).
Seed fix (FrameworkEditorControlTemplate.json)
- `documentTypes: null` → `documentTypes: []` for all 204 rows. The
schema requires a non-nullable `EvidenceFormType[]` (default `[]`)
but the export was writing nulls, which Prisma rejects on upsert.
- Mapped enum values from kebab-case (`"infrastructure-inventory"`)
to the TS-side snake_case (`"infrastructure_inventory"`). Prisma's
client expects the enum identifier, not the `@map`'d DB string.
Together these unblock `bun run db:seed` from a clean DB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(seed): refresh framework editor templates + finding templates
Updates the seed JSON exports to the latest snapshot. Touches:
- FindingTemplate: regenerated with current cuid-style ids and the
current title/content set the audit team is using.
- FrameworkEditorFramework / PolicyTemplate / Requirement /
TaskTemplate: refreshed primitives to match the live framework
editor state.
- Three Control↔(Task|Policy|Requirement) join files: refreshed
to match the new template ids.
bun.lock: dedup of @jridgewell/trace-mapping pulled in by `bun install`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(treatment-plan): hide linked work for non-mitigate + drop Avoid as a new selection
Reverts two changes I introduced for Cubic findings #36 / #37 — they
were technically correct readings of "ENG-221 keeps controls/tasks
visible" but the user-intent was the opposite: Linked Work and the
Avoid strategy are Mitigate-shaped concerns and shouldn't surface for
treatments that aren't operational reductions.
- TreatmentPlanTab: `showLinkedWorkColumn` is gated on `isMitigate &&
hasLinkedWork` again. Accept ("live with the risk"), Transfer
(insurance / contractual instruments), and Avoid (discontinue the
activity) are not control-driven mitigations, so the column adds
noise and a misread when shown.
- StrategyPicker: Avoid is back to legacy-only — never offered as a
new selection, but rendered for risks that are already set to
Avoid so existing state isn't dropped silently.
- ScoreExplainer: dropped the Avoid bullet and the Avoid mention in
the coverage-gate paragraph, matching what the picker now offers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(treatment-plan): url tabs, regen refresh, scroll caps, 4-per-page
UX fixes layered into a single commit so they ship together.
- RiskPageClient + VendorDetailTabs: active tab is now URL-backed via
useQueryState('tab'), and only the active panel is mounted. This
bookmarks the current tab in `?tab=...` (refresh keeps the view)
and eliminates the visible "both panels stacked" flash on switch
(base-ui keeps the outgoing panel at full opacity for the duration
of the incoming `fade-in-0 duration-200` animation).
- TreatmentPlanTab: drop the AutoMitigationPlaceholder ("AI is
preparing your treatment plan…"). Its heuristic stayed true
forever whenever the AI mitigation never wrote a description, so
users sat on an indefinite spinner. Falling through to
DescriptionEditor gives them the explicit "Generate treatment
plan" button.
- DescriptionEditor: regen-completion now bypasses the in-edit
draft-resync guard. When the user clicks "Regenerate with AI"
while in edit mode, they're explicitly opting into an overwrite,
but the existing guard kept the textarea showing the old draft
until refresh. Tracking regenRun's prev state and forcing
preview + setDraft(value) on transition fixes it.
- ScoreExplainer: cap the popover body at max-h-[70vh] with
overflow-y-auto. The narrative + formulas + references could
exceed the viewport on shorter screens with no way to reach the
bottom.
- AutoLinkSuggestions.sections: PAGE_SIZE 10 → 4 to match the new
LinkedWork pagination. Selection UI now feels consistent with
the post-apply view.
- LinkedWork: explicit list-none + pl-0 on the Tasks/Controls
`<ul>` to defeat an inherited list-style coming from somewhere
(random bullet + ~40px left padding on each row).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(treatment-plan): drop orphan dot + checkbox-spacer indent on control rows
Two visual bugs in the Controls list of AutoLinkSuggestions:
- When `c.code` is empty (some frameworks ship controls without a
code, e.g. "PCI"), the row rendered "· PCI" because the prefix
format was unconditional `${c.code} · ${c.name}`. Now we only
prepend the "code · " segment when a code exists.
- Each row had a 16x16 invisible spacer <span> meant to align the
control text with the task text below the task-row checkbox. In
practice it just pushed controls ~24px to the right (spacer +
gap), making them look indented relative to the section header
and the Tasks rows above. Dropped the spacer so controls now
align to the left edge of the column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(onboarding): calibrate vendor inherent-risk prompt so well-known SaaS isn't 10/10
The previous system prompt for the AI vendor extraction had zero
calibration guidance — just "return inherent_probability and
inherent_impact". Without a rubric, gpt-4.1-mini defaulted every
vendor to (very_likely × severe) = 25 → 10/10 CRITICAL. GitHub
landed at 10/10 even though the same vendor card shows it carries
SOC 2 + ISO 27001 + ISO 42001 + four other certifications.
Adds explicit calibration:
- Per-bucket definitions for inherent_probability (very_unlikely
through very_likely), each anchored to concrete vendor
archetypes (hyperscaler / SaaS-with-SOC-2 / no-attestation / etc).
- Per-bucket definitions for inherent_impact, anchored to data
classes (none / metadata / PII / auth-or-source-or-payments /
production-infrastructure).
- Explicit DEFAULT for residual: "leave residual = inherent unless
the user's answers describe their OWN compensating controls."
The vendor's certifications already feed into inherent — they
shouldn't be double-counted as residual reductions.
- Sanity-check sentence naming the well-known vendors that should
land at (unlikely, moderate) ≈ 3/10, not 10/10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(onboarding): score vendor inherent risk from user signals, not name lookups
Drop the hardcoded list of "well-known SaaS" vendor names from the
inherent-risk calibration prompt. Maintaining a list inside an LLM
prompt is brittle (every new well-known vendor is a prompt edit) and
asks the LLM to reason from its prior knowledge rather than from the
data we actually have.
The new prompt makes two things explicit:
1. The model is scoring from the USER'S answers only — it does NOT
have access to the vendor's public posture (SOC 2, ISO 27001,
incident history). A separate research step (research-vendor →
GlobalVendors) fills that in later, and a follow-up will use the
researched data to refine the per-org Vendor row's score.
2. Default to MIDDLE (possible × moderate ≈ 5/10) when no signal
exists. Only deviate when the user's answers contain explicit
signals — listed by category (lowers probability / raises
probability / lowers impact / raises impact). When the user
simply NAMES the vendor with no further context, return
(possible, moderate) and let the research step refine.
This fixes the "GitHub got 10/10" pathology without coupling the
prompt to a vendor name registry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(trigger): throttle slow tasks so they don't hog dev's 25-slot budget
Trigger.dev's dev environment is hardcoded at 25 concurrent runs (no
extra purchasable). With our onboarding fan-out (10 research-vendor +
21 mitigations + 30+ update-policy + a few coordinators) we routinely
queue 70+ jobs. The slow tasks were holding slots long enough that
the fast, user-visible mitigation tasks sat queued for minutes.
Two cap changes, both at the queue level so they're effective in dev
but don't hurt prod (which has 240 slots and won't notice):
- update-policy: 50 → 5. Each policy LLM update takes ~20-40s. With
no cap, 30+ policies firing simultaneously would fill every slot
for the entire policy window, starving generate-risk-mitigation
and generate-vendor-mitigation that fire right after.
- research-vendor: NEW queue, capped at 5. Each scrape can hold a
slot for minutes (firecrawl + LLM extraction). 10 of them firing
in parallel was eating 40% of the dev budget on its own.
Net effect: fast tasks (mitigations, ~10s each) get to run promptly
while slow tasks (policies ~25s, scrapes ~60s+) drain in the
background.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(treatment-plan): default strategy to mitigate + force mitigate when AI plan lands
Three layered bugs all rooted in the schema default + the AI plan
ending up under the wrong strategy slot:
1. Vendors (and risks) defaulted to `accept`. Most users want to
actively mitigate via controls/tasks, not accept the inherent
risk — accept is for documented exception cases.
2. The AI mitigation generator wrote its plan into the entity's
*current* strategy slot. With default = accept, the plan landed
under map[accept] AND treatmentStrategyDescription with strategy
= accept. The user saw their AI-generated mitigation plan
labeled as the "Accept rationale" — the symptom they reported.
3. Switching strategies looked broken because the plan was in the
wrong slot. Switching mitigate → accept showed the plan
correctly (it was stored under accept), and switching accept →
mitigate looked empty (nothing was stored under mitigate).
Fixes:
- Schema migration 20260506130119_default_treatment_strategy_to_
mitigate flips Risk.treatmentStrategy and Vendor.treatmentStrategy
defaults from `accept` to `mitigate`.
- New helper `applyMitigationPlanFields` in lib/strategy-descriptions
unconditionally writes the AI plan to map[mitigate] AND forces
treatmentStrategy=mitigate. Any prior non-mitigate description on
the entity is preserved under its own slot, so users on existing
rows with an Accept rationale don't lose their text.
- generate-risk-mitigation and generate-vendor-mitigation both use
the new helper instead of mirroring into whatever the active
strategy happens to be.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(onboarding): drop LLM-picked treatment strategy when creating risks
Even with the schema default flipped to mitigate, risks were still
landing on whatever strategy the extraction LLM picked
(`risk_treatment_strategy` in the structured output). The field is
ignored at the create-call sites now — risks are always created
with the schema default (mitigate) so the AI mitigation plan that
runs immediately after lands under the correct slot.
The field stays in the LLM schema/RiskData type so the prompt
doesn't change shape (small blast radius); cleanup of the dead
field is a separate refactor.
Both create paths covered:
- createRisksFromData (LLM-only)
- createRisksFromDataWithBaseline (LLM + baseline)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(vendors): residual column reflects current treatment progress, not static residual
Mirror the risks-table pattern. The Vendors table previously showed
the static `residualProbability × residualImpact` fields, which never
move once set — so a vendor with a treatment plan in progress always
looked the same as one with no progress. Now the column shows the
score interpolated by linked-task completion, identical to the
treatment-plan hero.
- API (apps/api/src/vendors/vendors.service.ts): findAllByOrganization
now `include`s `tasks: { select: { id, status } }`. Same shape the
risks service has.
- Frontend (VendorsTable.tsx): adds `currentVendorSeverityScore`
helper using `previewResidual` + `suggestedResidual` +
`interpolatedResidualScore`, mirroring the risks-side helper
exactly. The residual cell renders the badge from this score
instead of the static fields, and the residual sort uses the same
number so order matches the displayed values.
Result: a vendor on Mitigate with 50% of linked tasks complete
renders mid-way between inherent and target, like the hero numeral
on the vendor detail page already did.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(risks): add Inherent column to risks table
Three score columns now paint the before-vs-now picture explicitly:
- INHERENT — raw score before any treatment (fixed once at risk
creation). Renders as a colored chip showing the score.
- SEVERITY — current treatment-aware level (Low / Medium / High /
Critical / Negligible) as plain text.
- RISK SCORE — current treatment-aware score, interpolated by
linked-task completion. Colored chip carries the band so we
don't double-paint with SEVERITY.
Lets users see at a glance how far their treatment is moving each
risk (e.g. inherent 8 → current 5 means the treatment is doing
work). Mirrors the inherent + residual pair already visible on the
Vendors table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(risks): reorder columns — severity before inherent
Order is now: RISK | SEVERITY | INHERENT | RISK SCORE | STATUS | OWNER | UPDATED
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(risks): rename Risk Score column → Residual
Now matches the Vendors table naming and the standard inherent /
residual GRC vocabulary.
Order: RISK | SEVERITY | INHERENT | RESIDUAL | STATUS | OWNER | UPDATED
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tables): rename residual column to current
The score we render in this column is the interpolated current score
(updates with treatment progress), not the canonical residual (which
is the target at 100% completion — what the hero's right-side
arrow number shows). Calling it 'Residual' caused a real mismatch:
hero showed 4 → 2, table showed 4 → 3 for the same risk.
Renamed both tables for consistency. Sort key 'residualRisk' kept
as-is (URL-stable, internal); only the visible label changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(risks): suffix score columns with 'risk' for clarity
Both score columns now read 'INHERENT RISK' and 'CURRENT RISK',
matching the Vendors table convention where the same suffix is
already in use.
* fix: handle async regen value + label prose with mitigate (cubic 2764)
Two findings from cubic on PR #2764:
P1 — DescriptionEditor regen async-arrival:
When a regenerate-with-AI run completed, the parent flipped
`regenRun` to null and called `mutateRisk()`. The new AI prose
could arrive in either:
(a) the same render (sync) — `value` already updated, OR
(b) a later render after SWR refetched — `value` lagged.
My previous fix only handled (a): on the regenRun set→null
transition, it called setDraft(value) once. If the prose was
arriving via (b), value was still stale at that moment, the
immediate setDraft applied the OLD value, and the resync
effect that would normally pick up the new value skipped
because mode === 'edit'. Net effect: user sat on the old text
until refresh.
Fix captures value-at-regen-clear-time in a ref. A second
effect watches for value to differ from the captured snapshot
and applies the new prose then. Both sync and async paths
now land the AI text.
P2 — Mitigation prose labeled with old strategy:
`combineSentencesWithCitations` builds the prose with header
`Treatment plan (${treatmentStrategy})`, but
`applyMitigationPlanFields` forces the saved
`treatmentStrategy` to 'mitigate'. So a row that was on
'accept' before the AI plan ran ended up with prose labeled
"Treatment plan (accept)" stored under strategy=mitigate.
Pin the prose label + the strategy in the LLM prompt to
'mitigate' (literal) at both call sites — risk and vendor
generators. Same source of truth as the persistence side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Mariano <marfuen98@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 06e218b commit 8a1c46f
11 files changed
Lines changed: 243 additions & 54 deletions
File tree
- apps
- api/src/vendors
- app/src
- app/(app)/[orgId]
- risk/(overview)
- vendors/(overview)/components
- components/risks/treatment-plan
- lib
- trigger/tasks
- onboarding
- scrape
- packages/db/prisma
- migrations/20260506130119_default_treatment_strategy_to_mitigate
- schema
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
110 | 115 | | |
111 | 116 | | |
112 | 117 | | |
| |||
Lines changed: 18 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
625 | 625 | | |
626 | 626 | | |
627 | 627 | | |
628 | | - | |
| 628 | + | |
| 629 | + | |
629 | 630 | | |
630 | 631 | | |
631 | 632 | | |
| |||
658 | 659 | | |
659 | 660 | | |
660 | 661 | | |
661 | | - | |
662 | | - | |
663 | | - | |
664 | | - | |
665 | | - | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
666 | 675 | | |
667 | 676 | | |
668 | 677 | | |
669 | 678 | | |
670 | 679 | | |
671 | 680 | | |
672 | 681 | | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
673 | 685 | | |
674 | 686 | | |
675 | 687 | | |
| |||
Lines changed: 52 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
9 | 15 | | |
10 | 16 | | |
11 | 17 | | |
| |||
54 | 60 | | |
55 | 61 | | |
56 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
57 | 97 | | |
58 | 98 | | |
59 | 99 | | |
| |||
349 | 389 | | |
350 | 390 | | |
351 | 391 | | |
352 | | - | |
353 | | - | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
354 | 397 | | |
355 | 398 | | |
356 | 399 | | |
| |||
571 | 614 | | |
572 | 615 | | |
573 | 616 | | |
574 | | - | |
| 617 | + | |
575 | 618 | | |
576 | 619 | | |
577 | 620 | | |
| |||
610 | 653 | | |
611 | 654 | | |
612 | 655 | | |
613 | | - | |
614 | | - | |
615 | | - | |
616 | | - | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
617 | 662 | | |
618 | 663 | | |
619 | 664 | | |
| |||
Lines changed: 35 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
116 | | - | |
117 | | - | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
118 | 133 | | |
| 134 | + | |
119 | 135 | | |
120 | | - | |
121 | | - | |
| 136 | + | |
| 137 | + | |
122 | 138 | | |
123 | 139 | | |
| 140 | + | |
| 141 | + | |
124 | 142 | | |
125 | 143 | | |
126 | 144 | | |
127 | 145 | | |
128 | 146 | | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
129 | 160 | | |
130 | 161 | | |
131 | 162 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
0 commit comments