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
30 changes: 19 additions & 11 deletions src/pages/courts/CourtReportCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
CourtStandingReference,
CourtTargetPreview,
CourtTriggerCounter,
CourtTriggerRequirement,
formatCourtInstant,
} from "./components/CourtPrimitives";
import {
Expand All @@ -50,6 +51,7 @@ import {
courtOffenseDisplay,
courtReportLaneChoiceLabel,
courtReportRouteDescription,
courtReportTriggerRequirement,
} from "./model/courtPresentation";
import {
courtErrorIssue,
Expand Down Expand Up @@ -256,6 +258,13 @@ const CourtReportCreate: React.FC = () => {
const selectedReason = capability?.reasonCapabilities.find(
({ reason }) => `${reason.offenseCode}:${reason.lane}` === reasonKey,
);
const triggerRequirement = selectedReason
? courtReportTriggerRequirement(
selectedReason.reason.lane,
selectedReason.standing,
capability?.population,
)
: null;
const protectiveReview = selectedReason?.protectiveReview;
const protectiveReviewAvailable =
protectiveReview?.eligible === true && !incidentEndsAt;
Expand Down Expand Up @@ -547,26 +556,25 @@ const CourtReportCreate: React.FC = () => {
</p>
{selectedReason ? (
<div className="space-y-2 md:col-span-2">
{selectedReason.reason.lane === "court_report" &&
!selectedReason.standing.directStanding ? (
capability?.population?.communityThreshold ? (
{triggerRequirement?.kind === "community" ? (
triggerRequirement.required ? (
<CourtTriggerCounter
required={capability.population.communityThreshold}
viewerCounts={
capability.population.viewerCountsTowardCommunity
}
required={triggerRequirement.required}
viewerCounts={triggerRequirement.viewerCounts}
/>
) : (
<p className="text-sm leading-6 text-muted">
A community Court trigger is unavailable because fewer
than three eligible Governors remain after excluding the
The Governor threshold is unavailable because fewer than
three eligible Governors remain after excluding the
respondent.
</p>
)
) : triggerRequirement?.kind === "authority" ? (
<CourtTriggerRequirement {...triggerRequirement} />
) : (
<CourtTriggerCounter
description="One admissible report routes this action to its responsible authority."
label="Admissible reports"
description={triggerRequirement?.description}
label={triggerRequirement?.label}
required={1}
/>
)}
Expand Down
6 changes: 4 additions & 2 deletions src/pages/courts/CourtReportDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,10 @@ const CourtReportDetail: React.FC = () => {
<GlassySection title="Report status">
<GlassyTile className="space-y-4">
<CourtStateSummary
description={courtReportStateDisplay(report.state).description}
label={courtReportStateDisplay(report.state).label}
description={
courtReportStateDisplay(report.state, report.lane).description
}
label={courtReportStateDisplay(report.state, report.lane).label}
tone={courtTone(report.state)}
/>
<CourtReportActionStatus report={report} />
Expand Down
20 changes: 20 additions & 0 deletions src/pages/courts/components/CourtPrimitives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,26 @@ export function CourtTriggerCounter({
);
}

export function CourtTriggerRequirement({
description,
label,
value,
}: {
description: string;
label: string;
value: string;
}) {
return (
<div className="space-y-2 border-y border-border/70 py-3">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<p className="text-sm font-medium text-text">{label}</p>
<p className="text-sm font-semibold text-text">{value}</p>
</div>
<p className="text-xs leading-5 text-muted">{description}</p>
</div>
);
}

export function CourtReportActionStatus({
report,
}: {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/courts/components/CourtRecordCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function CourtReportCard({
report: CourtMyReportItemV2Dto;
}) {
const offense = courtOffenseDisplay(report.offenseCode);
const state = courtReportStateDisplay(report.state);
const state = courtReportStateDisplay(report.state, report.lane);
const lane = courtLaneDisplay(report.lane);
return (
<GlassyTile className="flex min-h-56 flex-col gap-4">
Expand Down
75 changes: 74 additions & 1 deletion src/pages/courts/model/courtPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ export type CourtReportActionProgress = {
viewerCounts?: boolean;
};

export type CourtReportTriggerRequirement =
| {
kind: "community";
required: number | null;
viewerCounts: boolean;
}
| {
kind: "single";
description: string;
label: string;
}
| {
kind: "authority";
description: string;
label: string;
value: string;
};

export type CourtStandingDisplay = CourtDisplayEntry & {
verification: string;
};
Expand Down Expand Up @@ -95,6 +113,42 @@ export function courtReportRouteDescription(
: "This enters the private community trigger. A case opens only after the protected reporting threshold is reached.";
}

export function courtReportTriggerRequirement(
lane: CourtReportLaneV2Dto,
standing: { direct?: boolean; directStanding?: boolean },
population?: {
communityThreshold: number | null;
viewerCountsTowardCommunity: boolean;
} | null,
): CourtReportTriggerRequirement {
const direct = standing.direct ?? standing.directStanding ?? false;
if (lane === "correction" || (lane === "court_report" && !direct)) {
return {
kind: "community",
required: population?.communityThreshold ?? null,
viewerCounts: population?.viewerCountsTowardCommunity ?? false,
};
}
if (lane === "safety_or_protocol_incident") {
return {
kind: "authority",
label: "Court case trigger",
value: "Verified proof or authorized referral",
description:
"Governor report counts do not open a case on this lane. Evidence remains preserved for an authorized safety or protocol process.",
};
}
return {
kind: "single",
label:
lane === "scoped_moderation" ? "Moderation action" : "Admissible reports",
description:
lane === "scoped_moderation"
? "One admissible report routes the record to its authorized moderation process."
: "Verified direct standing allows one admissible report to enter Court review.",
};
}

export function courtReportActionProgress(
report: Pick<CourtMyReportItemV2Dto, "lane" | "state" | "triggerProgress">,
): CourtReportActionProgress | null {
Expand Down Expand Up @@ -154,12 +208,20 @@ export function courtReportProcessContext(
"Follow the linked case for notice, evidence, decision, and appeal.",
};
}
if (report.lane === "safety_or_protocol_incident") {
return {
basis,
destination: "Safety and protocol intake",
nextStep:
"The evidence remains preserved for authorized review. A Court case can open only through verified objective proof or an authorized emergency referral.",
};
}
if (report.state === "collecting" || report.state === "grouped") {
return {
basis,
destination: "Private incident collection",
nextStep:
"The report remains active without revealing other reporters or trigger thresholds.",
"The report remains active while reporter identities stay private. Aggregate Governor progress updates as matching reports qualify.",
};
}
if (report.state === "needs_amendment") {
Expand Down Expand Up @@ -434,7 +496,18 @@ export function courtOffenseDisplay(

export function courtReportStateDisplay(
state: CourtReportStateV2Dto,
lane?: CourtReportLaneV2Dto,
): CourtDisplayEntry {
if (
lane === "safety_or_protocol_incident" &&
(state === "collecting" || state === "grouped")
) {
return {
label: "Safety intake",
description:
"The incident is preserved for authorized safety or protocol review. Governor report counts do not open a Court case on this lane.",
};
}
return REPORT_STATES[state];
}

Expand Down
117 changes: 104 additions & 13 deletions tests/e2e/courts-v2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ async function installCourtFixtures(
notifications?: Record<string, unknown>[];
onCapability?: (url: URL) => void;
onCommand?: (command: Record<string, unknown>) => void;
reasonCapabilities?: Record<string, unknown>[];
reportDetail?: Record<string, unknown>;
reports?: Record<string, unknown>[];
reportsStatus?: "available" | "unavailable";
Expand Down Expand Up @@ -333,7 +334,7 @@ async function installCourtFixtures(
affectedId: "hmrCurrentReporter",
affectedIdSource: "direct_reporter",
},
reasonCapabilities: [
reasonCapabilities: options.reasonCapabilities ?? [
{
reason: { offenseCode: "GOV-03", lane: "court_report" },
standing: {
Expand Down Expand Up @@ -708,6 +709,90 @@ test("community report creation shows the Governor threshold and contribution",
await expect(page.getByText(/Governor population: 99/)).toBeVisible();
});

test("report creation explains each lane's actual trigger semantics", async ({
page,
}) => {
const standing = (directStanding: boolean) => ({
status: "verified",
directStanding,
source: directStanding ? "target-owner" : "active-governor",
});
await installCourtFixtures(page, caseRecord, true, {
capabilityPopulation: {
source: "vortex-governor-roster:fixture",
basis: "capture_time_fallback",
effectiveAt: "2026-08-12T10:00:00.000Z",
capturedAt: "2026-08-12T10:00:00.000Z",
eligibleGovernorCount: 99,
communityThreshold: 10,
viewerCountsTowardCommunity: true,
},
reasonCapabilities: [
{
reason: { offenseCode: "CMP-01", lane: "correction" },
standing: standing(true),
protectiveReview: { eligible: false },
},
{
reason: { offenseCode: "CMP-03", lane: "scoped_moderation" },
standing: standing(true),
protectiveReview: { eligible: false },
},
{
reason: { offenseCode: "GOV-03", lane: "court_report" },
standing: standing(true),
protectiveReview: { eligible: false },
},
{
reason: {
offenseCode: "SEC-03",
lane: "safety_or_protocol_incident",
},
standing: standing(true),
protectiveReview: {
eligible: true,
authorityIds: ["protocol-safety-council"],
durationSeconds: 86_400,
},
},
],
});
await page.goto(
"/app/courts/reports/new?targetType=proposal&targetId=proposal-under-review",
);

const reason = page.getByLabel("Reason", { exact: true });
await reason.selectOption("CMP-01:correction");
await expect(
page.getByText("Governor reports", { exact: true }),
).toBeVisible();
await expect(page.getByText("10 required", { exact: true })).toBeVisible();

await reason.selectOption("CMP-03:scoped_moderation");
await expect(
page.getByText("Moderation action", { exact: true }),
).toBeVisible();
await expect(page.getByText("1 required", { exact: true })).toBeVisible();

await reason.selectOption("GOV-03:court_report");
await expect(
page.getByText("Admissible reports", { exact: true }),
).toBeVisible();
await expect(page.getByText("1 required", { exact: true })).toBeVisible();

await reason.selectOption("SEC-03:safety_or_protocol_incident");
await expect(
page.getByText("Court case trigger", { exact: true }),
).toBeVisible();
await expect(
page.getByText("Verified proof or authorized referral", { exact: true }),
).toBeVisible();
await expect(page.getByText("Governor reports", { exact: true })).toHaveCount(
0,
);
await expect(page.getByText("10 required", { exact: true })).toHaveCount(0);
});

for (const width of [390, 1440]) {
test(`dense report creation stays readable at ${width}px`, async ({
page,
Expand Down Expand Up @@ -782,23 +867,29 @@ test("every reporter state has visible next-step guidance", async ({
route: null,
},
offenseCode:
state === "routed_to_correction"
? "CMP-01"
: index % 2 === 0
? "GOV-03"
: "CMP-03",
state === "grouped"
? "SEC-03"
: state === "routed_to_correction"
? "CMP-01"
: index % 2 === 0
? "GOV-03"
: "CMP-03",
lane:
state === "routed_to_correction"
? "correction"
: state === "routed_to_moderation"
? "scoped_moderation"
: "court_report",
state === "grouped"
? "safety_or_protocol_incident"
: state === "routed_to_correction"
? "correction"
: state === "routed_to_moderation"
? "scoped_moderation"
: "court_report",
submittedAt: "2026-08-01T10:00:00.000Z",
updatedAt: "2026-08-12T10:00:00.000Z",
caseId: state === "triggered" ? caseId : null,
respondentId: `human-respondent-${index}`,
triggerProgress:
state === "routed_to_correction" || state === "routed_to_moderation"
state === "routed_to_correction" ||
state === "routed_to_moderation" ||
state === "grouped"
? null
: {
qualifyingReports: 2,
Expand Down Expand Up @@ -828,7 +919,7 @@ test("every reporter state has visible next-step guidance", async ({
"Collecting",
"Routed to correction",
"Routed to moderation",
"Grouped",
"Safety intake",
"Withdrawn",
"Expired",
"Closed without a case",
Expand Down
Loading
Loading