Skip to content

[dev] [Marfuen] mariano/treatment-plan-scroll#2764

Merged
Marfuen merged 20 commits into
mainfrom
mariano/treatment-plan-scroll
May 6, 2026
Merged

[dev] [Marfuen] mariano/treatment-plan-scroll#2764
Marfuen merged 20 commits into
mainfrom
mariano/treatment-plan-scroll

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot commented May 6, 2026

This is an automated pull request to merge mariano/treatment-plan-scroll into dev.
It was created by the [Auto Pull Request] action.


Summary by cubic

Improves the Treatment Plan with paginated Linked Work and URL-backed tabs, stabilizes layouts, and ensures generated mitigation plans always save under Mitigate. Updates scoring UI: adds INHERENT RISK and renames the residual column to CURRENT RISK (both suffixed with “RISK”); vendor/current risk now reflects linked-task progress.

  • New Features

    • Linked Work pagination (4 per page); suggestions UI matches. Tabs use nuqs and only mount the active panel.
    • Tables: Risks show INHERENT RISK and CURRENT RISK; order is RISK | SEVERITY | INHERENT RISK | CURRENT RISK | STATUS | OWNER | UPDATED. Vendors show CURRENT RISK interpolated by linked‑task completion; API includes task statuses and sorting uses the same score.
    • Treatment Plan UX: capped editor/preview height, scrollable score explainer, cleaner lists; “Regenerate” applies the new text and switches to preview immediately, including async arrivals.
  • Bug Fixes

    • Strategy handling: default risks/vendors to mitigate; generated plans write under Mitigate and switch the active strategy; existing non‑mitigate text is preserved; the prose header now labels “mitigate” to match.
    • Strategy semantics: hide Linked Work for non‑mitigate; “Avoid” is not offered as a new choice (still shown for legacy risks).
    • Onboarding/perf: calibrated vendor inherent scoring; throttle @trigger.dev/sdk queues (update-policy 5; new research-vendor 5).
    • Seeds/lockfile: set documentTypes to [], fix enum mappings, refresh seed data, and dedupe @jridgewell/trace-mapping.

Written for commit e047ddf. Summary will update on new commits.

Marfuen and others added 9 commits May 6, 2026 12:14
…ight

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>
…+ 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>
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>
…s 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>
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>
…rol 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>
…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>
…ot 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>
…dget

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment May 6, 2026 2:14pm
comp-framework-editor Ready Ready Preview, Comment May 6, 2026 2:14pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
portal Skipped Skipped May 6, 2026 2:14pm

Request Review

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 21 files

Confidence score: 3/5

  • There is a concrete user-impacting risk in apps/app/src/components/risks/treatment-plan/DescriptionEditor.tsx: async treatment plans can be ignored without tracking pending AI responses.
  • Seed data includes placeholder templates in packages/db/prisma/seed/primitives/FrameworkEditorControlTemplate.json and packages/db/prisma/seed/primitives/FrameworkEditorTaskTemplate.json, which will persist as real compliance items if merged.
  • Given the high-severity async handling issue and persisted placeholder data, this carries some risk beyond a routine merge.
  • Pay close attention to apps/app/src/components/risks/treatment-plan/DescriptionEditor.tsx, packages/db/prisma/seed/primitives/FrameworkEditorControlTemplate.json, packages/db/prisma/seed/primitives/FrameworkEditorTaskTemplate.json - async response handling and removal of test seed data.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/db/prisma/seed/primitives/FrameworkEditorControlTemplate.json">

<violation number="1" location="packages/db/prisma/seed/primitives/FrameworkEditorControlTemplate.json:334">
P2: Remove placeholder/test control templates from the seed file before merge.</violation>
</file>

<file name="packages/db/prisma/seed/primitives/FrameworkEditorTaskTemplate.json">

<violation number="1" location="packages/db/prisma/seed/primitives/FrameworkEditorTaskTemplate.json:1334">
P2: Remove placeholder test task templates from the seed data; they will be persisted as real compliance tasks.</violation>
</file>

<file name="apps/app/src/components/risks/treatment-plan/DescriptionEditor.tsx">

<violation number="1" location="apps/app/src/components/risks/treatment-plan/DescriptionEditor.tsx:118">
P1: Track pending AI responses to avoid ignoring asynchronously arriving treatment plans.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread apps/app/src/components/risks/treatment-plan/DescriptionEditor.tsx
},
{
"id": "frk_ct_69becde6133891181f5fbfe1",
"name": "Fridge kept between 2-5",
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Remove placeholder/test control templates from the seed file before merge.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/prisma/seed/primitives/FrameworkEditorControlTemplate.json, line 334:

<comment>Remove placeholder/test control templates from the seed file before merge.</comment>

<file context>
@@ -4,349 +4,1699 @@
+  },
+  {
+    "id": "frk_ct_69becde6133891181f5fbfe1",
+    "name": "Fridge kept between 2-5",
+    "description": "Keep fridge between 2and 5degrees",
+    "createdAt": "2026-03-21 16:57:10.184",
</file context>
Fix with Cubic

},
{
"id": "frk_tt_69f10edce3013d5b6e0ab7c9",
"name": "Updated tasl",
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Remove placeholder test task templates from the seed data; they will be persisted as real compliance tasks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/prisma/seed/primitives/FrameworkEditorTaskTemplate.json, line 1334:

<comment>Remove placeholder test task templates from the seed data; they will be persisted as real compliance tasks.</comment>

<file context>
@@ -729,14 +719,764 @@
+  },
+  {
+    "id": "frk_tt_69f10edce3013d5b6e0ab7c9",
+    "name": "Updated tasl",
+    "description": "test",
+    "frequency": "monthly",
</file context>
Fix with Cubic

…en 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>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts">

<violation number="1" location="apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts:1043">
P2: Risk mitigation text is still labeled with the old strategy even though this change now forcibly switches the saved strategy to `mitigate`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

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>
…t 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>
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>
@vercel vercel Bot temporarily deployed to Preview – portal May 6, 2026 13:49 Inactive
Order is now: RISK | SEVERITY | INHERENT | RISK SCORE | STATUS | OWNER | UPDATED

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel vercel Bot temporarily deployed to Preview – portal May 6, 2026 13:49 Inactive
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>
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>
@vercel vercel Bot temporarily deployed to Preview – portal May 6, 2026 13:53 Inactive
Both score columns now read 'INHERENT RISK' and 'CURRENT RISK',
matching the Vendors table convention where the same suffix is
already in use.
Marfuen and others added 2 commits May 6, 2026 15:09
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>
@vercel vercel Bot temporarily deployed to Preview – portal May 6, 2026 14:10 Inactive
@Marfuen Marfuen merged commit 8a1c46f into main May 6, 2026
10 of 11 checks passed
@Marfuen Marfuen deleted the mariano/treatment-plan-scroll branch May 6, 2026 14:13
@claudfuen
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.44.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants