Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions docs/maturity-backlog-workorders.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ structural change, not a single mixed PR.
### X3 · Decompose the monoliths — `IN PROGRESS`

- **Outcome:** shrink the three files the maintainability ratchet caps but never reduces:
`src/lib/rag/rag.ts` (5,018), `src/components/ClinicalDashboard.tsx` (4,271),
`src/lib/rag/rag.ts` (5,018), `src/components/ClinicalDashboard.tsx` (was 4,271, now 4,157),
`src/components/DocumentViewer.tsx` (was 3,164, now 1,733).
- **Progress (#997):** extracted the evidence-gate predicates from `rag.ts` into
`src/lib/rag/rag-evidence-gates.ts` (rag.ts 5,147 → 5,018), pure moves behind the existing
Expand All @@ -87,8 +87,14 @@ structural change, not a single mixed PR.
behaviour-bearing `manual-tag-editor.tsx` (add/rename/delete manual labels), and
`document-overview-landing.tsx`. The moves are verbatim (no logic changed); the container is
now composition-focused — it retains the detail fetch, dynamic PDF loading, and state
orchestration (3,164 → 1,733, budget ratcheted to 1,733). `ClinicalDashboard.tsx` (4,271)
and `rag.ts` remain the open decomposition targets.
orchestration (3,164 → 1,733, budget ratcheted to 1,733).
- **Progress (`ClinicalDashboard.tsx`):** the dashboard was already heavily decomposed (72
`clinical-dashboard/*` modules), so this is an incremental ratchet — extracted the
self-contained answer-thread turn leaf (`AnswerTurn` type, `maxVisiblePriorTurns`, and the
`PriorAnswerTurnSurface` component) into `clinical-dashboard/answer-thread-turn.tsx` as a
verbatim move (4,271 → 4,157, budget ratcheted to 4,157). The residual is a tightly-coupled
orchestrator core; further safe extractions are smaller, incremental units. `rag.ts` remains
the largest open target.
- **Approach:** extract cohesive units behind the existing budgets; the components decompose
into their `*/` sibling directories, and `rag.ts` is the natural seam now that X2 has landed.
- **Risk:** HIGH (behavioural surface). One file per PR.
Expand Down Expand Up @@ -198,18 +204,18 @@ collaborators join — `AGENTS.md` + the PR template already carry that load.

## Progress summary

| Item | Priority | Status |
| ------------------------------ | -------- | ------------------------------------------- |
| N1 Dependabot grouping | Now | **DONE** (#985) |
| N2 Dependency-report decision | Now | **DONE** (#986, enabled) |
| X1 Import-boundary linter | Next | **DONE** (#986; service-role rule dropped) |
| X2 `src/lib` rag extraction | Next | **DONE** (#994) |
| X3 Monolith decomposition | Next | IN PROGRESS (rag #997; DocumentViewer done) |
| X4 SAST-blocking on parser | Next | **DONE** (gate + policy check) |
| X5 ACL-migration consolidation | Next | PROVIDER-GATED (DB owner) |
| X6 Coverage floors | Next | OPEN |
| L1 Archive one-shot scripts | Later | OPEN (index shipped) |
| L2 Action-SHA uniformity | Later | **DONE** (#992) |
| L3 Single gate manifest | Later | **DONE** (#1002) |
| L4 Ledger rotation | Later | OPEN |
| L5 AI map / WCAG / RPO-RTO | Later | **DONE / SATISFIED** (#985) |
| Item | Priority | Status |
| ------------------------------ | -------- | ------------------------------------------------------- |
| N1 Dependabot grouping | Now | **DONE** (#985) |
| N2 Dependency-report decision | Now | **DONE** (#986, enabled) |
| X1 Import-boundary linter | Next | **DONE** (#986; service-role rule dropped) |
| X2 `src/lib` rag extraction | Next | **DONE** (#994) |
| X3 Monolith decomposition | Next | IN PROGRESS (rag #997; DocumentViewer + Dashboard done) |
| X4 SAST-blocking on parser | Next | **DONE** (gate + policy check) |
| X5 ACL-migration consolidation | Next | PROVIDER-GATED (DB owner) |
| X6 Coverage floors | Next | OPEN |
| L1 Archive one-shot scripts | Later | OPEN (index shipped) |
| L2 Action-SHA uniformity | Later | **DONE** (#992) |
| L3 Single gate manifest | Later | **DONE** (#1002) |
| L4 Ledger rotation | Later | OPEN |
| L5 AI map / WCAG / RPO-RTO | Later | **DONE / SATISFIED** (#985) |
2 changes: 1 addition & 1 deletion scripts/check-maintainability-budgets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { readFileSync } from "node:fs";

const budgets = new Map([
["src/components/ClinicalDashboard.tsx", 4272],
["src/components/ClinicalDashboard.tsx", 4157],
["src/lib/rag/rag.ts", 5030],
["src/components/DocumentViewer.tsx", 1733],
["supabase/functions/indexing-v3-agent/index.ts", 2191],
Expand Down
126 changes: 6 additions & 120 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import dynamic from "next/dynamic";
import {
CircleAlert,
BookOpen,
ChevronDown,
Clock3,
ExternalLink,
FileImage,
Expand Down Expand Up @@ -42,7 +41,6 @@ import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local
import { isDeployedClinicalKb } from "@/lib/deployed-app";
import {
appBackdrop,
answerSurface,
cn,
EmptyState,
floatingControl,
Expand Down Expand Up @@ -77,12 +75,7 @@ import {
} from "@/components/clinical-dashboard/DocumentManagerPanel";
import { GuideDialog, GuideTrigger, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell";
import { sanitizeAnswerDisplayText, sanitizeDisplayText } from "@/components/clinical-dashboard/display-text";
import {
isPreformattedGroundedAnswer,
NaturalLanguageAnswer,
ScopeAndGovernanceNotice,
UserQuestionBubble,
} from "@/components/clinical-dashboard/answer-content";
import { isPreformattedGroundedAnswer, ScopeAndGovernanceNotice } from "@/components/clinical-dashboard/answer-content";
import { AnswerEmptyState, AnswerProgressStepper, AnswerSkeleton } from "@/components/clinical-dashboard/answer-status";
import {
type AnswerProgressUpdate,
Expand Down Expand Up @@ -253,6 +246,11 @@ import type { SearchScopeFilters } from "@/lib/search-scope";
import { differentialsMobileCompareAddonSlotId, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
import { toolCatalogRecords } from "@/lib/tools-catalog";
import { createQuoteFollowUp, type AnswerViewMode, shouldPollForUpdates } from "@/lib/ward-output";
import {
type AnswerTurn,
maxVisiblePriorTurns,
PriorAnswerTurnSurface,
} from "@/components/clinical-dashboard/answer-thread-turn";

const documentPageSize = 150;
const activeIndexingPollFallbackMs = 5_000;
Expand Down Expand Up @@ -335,118 +333,6 @@ type SearchResultModePayload =

type SourceLibrarySearchMode = Extract<AppModeSearchKind, "documents" | "differentials">;

/**
* A completed Q&A exchange kept on screen after a newer answer arrives, so
* Answer mode reads as a conversation thread instead of replacing each result.
*/
type AnswerTurn = {
id: string;
query: string;
answer: RagAnswer;
sources: SearchResult[];
};

const maxVisiblePriorTurns = 10;

/**
* Renders a collapsible, read-only view of a previous answer-thread turn with its question, answer, sources, and source-review notice.
*
* @param turn - The previous question and answer turn to display
* @param copied - Whether the turn's answer has been copied
* @param collapsed - Whether the answer content is collapsed
* @param onToggleCollapsed - Called when the answer visibility is toggled
* @param onCopy - Called with the answer text when copying is requested
*/
function PriorAnswerTurnSurface({
turn,
copied,
collapsed,
onToggleCollapsed,
onCopy,
}: {
turn: AnswerTurn;
copied: boolean;
collapsed: boolean;
onToggleCollapsed: () => void;
onCopy: (text: string) => void;
}) {
const renderModel = useMemo(
() => buildAnswerRenderModel(turn.answer, { sources: turn.sources }),
[turn.answer, turn.sources],
);
const turnPreformatted = isPreformattedGroundedAnswer(turn.answer);
const safeText = useMemo(
() => sanitizeAnswerDisplayText(turn.answer.answer, { preformatted: turnPreformatted }),
[turn.answer.answer, turnPreformatted],
);
const sourceCount =
renderModel.primarySources.length ||
turn.sources.length ||
turn.answer.sources?.length ||
turn.answer.citations.length;
const previewText = safeText || turn.answer.answer;
const needsSourceReview =
turn.answer.answerQualityTier === "source_only" ||
turn.answer.grounded === false ||
renderModel.trust === "low" ||
renderModel.trust === "unsupported";

return (
<div
// Historical conversation turns grow unbounded and most are collapsed and
// scrolled off-screen; content-auto skips their layout/paint until near the
// viewport. Safe here — the surface has no overflowing popovers, and the
// expand toggle is only reachable once the turn is scrolled into view.
className="content-auto min-w-0 space-y-4 sm:space-y-5"
data-dashboard-stage="answer-thread-turn"
data-collapsed={collapsed ? "true" : "false"}
>
<div className={cn(answerSurface, "space-y-3 p-2.5 sm:p-3")}>
<UserQuestionBubble query={turn.query} />
<button
type="button"
onClick={onToggleCollapsed}
aria-expanded={!collapsed}
className="inline-flex min-h-tap items-center gap-1.5 rounded-md px-1 text-xs font-semibold text-[color:var(--text-muted)] transition hover:text-[color:var(--text-heading)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
>
<ChevronDown className={cn("h-4 w-4 transition-transform", !collapsed && "rotate-180")} aria-hidden="true" />
{collapsed ? "Show previous answer" : "Hide previous answer"}
</button>
{collapsed ? (
<p className={cn("line-clamp-2 text-sm leading-6", textMuted)}>{previewText}</p>
) : (
<>
<NaturalLanguageAnswer
text={turn.answer.answer}
preformatted={turnPreformatted}
sourceCount={sourceCount}
sourceOnly={turn.answer.answerQualityTier === "source_only"}
bestSource={renderModel.bestSource}
sources={renderModel.reviewSources}
sourceLinks={renderModel.primarySources}
copied={copied}
onCopy={() => onCopy(renderModel.copyText || previewText)}
/>
{needsSourceReview ? (
<div
role="note"
data-testid="prior-answer-source-review"
className="mt-2 flex items-start gap-2 rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] px-3 py-2 text-xs text-[color:var(--text-muted)]"
>
<CircleAlert className="mt-0.5 h-4 w-4 shrink-0 text-[color:var(--warning)]" aria-hidden />
<span>
<strong className="text-[color:var(--text-heading)]">Review source match.</strong> Verify cited
passages before relying on this previous answer.
</span>
</div>
) : null}
</>
)}
</div>
</div>
);
}

type LibraryHealthTarget = "documents" | "setup" | "indexing" | "failures";
type IndexingMonitorFilter = "all" | "active" | "failed";
type UploadIndexingTab = "setup" | "upload" | "jobs" | "quality";
Expand Down
126 changes: 126 additions & 0 deletions src/components/clinical-dashboard/answer-thread-turn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// A completed Q&A exchange kept on screen after a newer answer arrives (the
// answer-thread turn) and its collapsible read-only surface. Extracted from
// ClinicalDashboard.tsx (maturity X3) as a pure move.
import { useMemo } from "react";
import { ChevronDown, CircleAlert } from "lucide-react";
import { buildAnswerRenderModel } from "@/lib/answer-render-policy";
import {
isPreformattedGroundedAnswer,
NaturalLanguageAnswer,
UserQuestionBubble,
} from "@/components/clinical-dashboard/answer-content";
import { sanitizeAnswerDisplayText } from "@/components/clinical-dashboard/display-text";
import { answerSurface, cn, textMuted } from "@/components/ui-primitives";
import type { RagAnswer, SearchResult } from "@/lib/types";

/**
* A completed Q&A exchange kept on screen after a newer answer arrives, so
* Answer mode reads as a conversation thread instead of replacing each result.
*/
export type AnswerTurn = {
id: string;
query: string;
answer: RagAnswer;
sources: SearchResult[];
};

export const maxVisiblePriorTurns = 10;

/**
* Renders a collapsible, read-only view of a previous answer-thread turn with its question, answer, sources, and source-review notice.
*
* @param turn - The previous question and answer turn to display
* @param copied - Whether the turn's answer has been copied
* @param collapsed - Whether the answer content is collapsed
* @param onToggleCollapsed - Called when the answer visibility is toggled
* @param onCopy - Called with the answer text when copying is requested
*/
export function PriorAnswerTurnSurface({
turn,
copied,
collapsed,
onToggleCollapsed,
onCopy,
}: {
turn: AnswerTurn;
copied: boolean;
collapsed: boolean;
onToggleCollapsed: () => void;
onCopy: (text: string) => void;
}) {
const renderModel = useMemo(
() => buildAnswerRenderModel(turn.answer, { sources: turn.sources }),
[turn.answer, turn.sources],
);
const turnPreformatted = isPreformattedGroundedAnswer(turn.answer);
const safeText = useMemo(
() => sanitizeAnswerDisplayText(turn.answer.answer, { preformatted: turnPreformatted }),
[turn.answer.answer, turnPreformatted],
);
const sourceCount =
renderModel.primarySources.length ||
turn.sources.length ||
turn.answer.sources?.length ||
turn.answer.citations.length;
const previewText = safeText || turn.answer.answer;
const needsSourceReview =
turn.answer.answerQualityTier === "source_only" ||
turn.answer.grounded === false ||
renderModel.trust === "low" ||
renderModel.trust === "unsupported";

return (
<div
// Historical conversation turns grow unbounded and most are collapsed and
// scrolled off-screen; content-auto skips their layout/paint until near the
// viewport. Safe here — the surface has no overflowing popovers, and the
// expand toggle is only reachable once the turn is scrolled into view.
className="content-auto min-w-0 space-y-4 sm:space-y-5"
data-dashboard-stage="answer-thread-turn"
data-collapsed={collapsed ? "true" : "false"}
>
<div className={cn(answerSurface, "space-y-3 p-2.5 sm:p-3")}>
<UserQuestionBubble query={turn.query} />
<button
type="button"
onClick={onToggleCollapsed}
aria-expanded={!collapsed}
className="inline-flex min-h-tap items-center gap-1.5 rounded-md px-1 text-xs font-semibold text-[color:var(--text-muted)] transition hover:text-[color:var(--text-heading)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
>
<ChevronDown className={cn("h-4 w-4 transition-transform", !collapsed && "rotate-180")} aria-hidden="true" />
{collapsed ? "Show previous answer" : "Hide previous answer"}
</button>
{collapsed ? (
<p className={cn("line-clamp-2 text-sm leading-6", textMuted)}>{previewText}</p>
) : (
<>
<NaturalLanguageAnswer
text={turn.answer.answer}
preformatted={turnPreformatted}
sourceCount={sourceCount}
sourceOnly={turn.answer.answerQualityTier === "source_only"}
bestSource={renderModel.bestSource}
sources={renderModel.reviewSources}
sourceLinks={renderModel.primarySources}
copied={copied}
onCopy={() => onCopy(renderModel.copyText || previewText)}
/>
{needsSourceReview ? (
<div
role="note"
data-testid="prior-answer-source-review"
className="mt-2 flex items-start gap-2 rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] px-3 py-2 text-xs text-[color:var(--text-muted)]"
>
<CircleAlert className="mt-0.5 h-4 w-4 shrink-0 text-[color:var(--warning)]" aria-hidden />
<span>
<strong className="text-[color:var(--text-heading)]">Review source match.</strong> Verify cited
passages before relying on this previous answer.
</span>
</div>
) : null}
</>
)}
</div>
</div>
);
}
Loading