[dev] [Marfuen] mariano/treatment-plan-scroll#2764
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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.jsonandpackages/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.
| }, | ||
| { | ||
| "id": "frk_ct_69becde6133891181f5fbfe1", | ||
| "name": "Fridge kept between 2-5", |
There was a problem hiding this comment.
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>
| }, | ||
| { | ||
| "id": "frk_tt_69f10edce3013d5b6e0ab7c9", | ||
| "name": "Updated tasl", |
There was a problem hiding this comment.
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>
…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>
There was a problem hiding this comment.
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>
Order is now: RISK | SEVERITY | INHERENT | RISK SCORE | STATUS | OWNER | UPDATED Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Both score columns now read 'INHERENT RISK' and 'CURRENT RISK', matching the Vendors table convention where the same suffix is already in use.
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>
…nto mariano/treatment-plan-scroll
|
🎉 This PR is included in version 3.44.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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
nuqsand only mount the active panel.Bug Fixes
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.@trigger.dev/sdkqueues (update-policy5; newresearch-vendor5).documentTypesto[], fix enum mappings, refresh seed data, and dedupe@jridgewell/trace-mapping.Written for commit e047ddf. Summary will update on new commits.