diff --git a/src/pages/courts/CourtReportCreate.tsx b/src/pages/courts/CourtReportCreate.tsx
index 493ceb3..aa9ae25 100644
--- a/src/pages/courts/CourtReportCreate.tsx
+++ b/src/pages/courts/CourtReportCreate.tsx
@@ -27,6 +27,7 @@ import {
CourtStandingReference,
CourtTargetPreview,
CourtTriggerCounter,
+ CourtTriggerRequirement,
formatCourtInstant,
} from "./components/CourtPrimitives";
import {
@@ -50,6 +51,7 @@ import {
courtOffenseDisplay,
courtReportLaneChoiceLabel,
courtReportRouteDescription,
+ courtReportTriggerRequirement,
} from "./model/courtPresentation";
import {
courtErrorIssue,
@@ -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;
@@ -547,26 +556,25 @@ const CourtReportCreate: React.FC = () => {
{selectedReason ? (
- {selectedReason.reason.lane === "court_report" &&
- !selectedReason.standing.directStanding ? (
- capability?.population?.communityThreshold ? (
+ {triggerRequirement?.kind === "community" ? (
+ triggerRequirement.required ? (
) : (
- 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.
)
+ ) : triggerRequirement?.kind === "authority" ? (
+
) : (
)}
diff --git a/src/pages/courts/CourtReportDetail.tsx b/src/pages/courts/CourtReportDetail.tsx
index ff9bee6..383b734 100644
--- a/src/pages/courts/CourtReportDetail.tsx
+++ b/src/pages/courts/CourtReportDetail.tsx
@@ -329,8 +329,10 @@ const CourtReportDetail: React.FC = () => {
diff --git a/src/pages/courts/components/CourtPrimitives.tsx b/src/pages/courts/components/CourtPrimitives.tsx
index a578a22..caaa59b 100644
--- a/src/pages/courts/components/CourtPrimitives.tsx
+++ b/src/pages/courts/components/CourtPrimitives.tsx
@@ -169,6 +169,26 @@ export function CourtTriggerCounter({
);
}
+export function CourtTriggerRequirement({
+ description,
+ label,
+ value,
+}: {
+ description: string;
+ label: string;
+ value: string;
+}) {
+ return (
+
+ );
+}
+
export function CourtReportActionStatus({
report,
}: {
diff --git a/src/pages/courts/components/CourtRecordCards.tsx b/src/pages/courts/components/CourtRecordCards.tsx
index 4a3e2f0..e19d0c9 100644
--- a/src/pages/courts/components/CourtRecordCards.tsx
+++ b/src/pages/courts/components/CourtRecordCards.tsx
@@ -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 (
diff --git a/src/pages/courts/model/courtPresentation.ts b/src/pages/courts/model/courtPresentation.ts
index 8b7cc97..4db8e29 100644
--- a/src/pages/courts/model/courtPresentation.ts
+++ b/src/pages/courts/model/courtPresentation.ts
@@ -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;
};
@@ -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,
): CourtReportActionProgress | null {
@@ -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") {
@@ -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];
}
diff --git a/tests/e2e/courts-v2.spec.ts b/tests/e2e/courts-v2.spec.ts
index cb7db00..c1a4966 100644
--- a/tests/e2e/courts-v2.spec.ts
+++ b/tests/e2e/courts-v2.spec.ts
@@ -208,6 +208,7 @@ async function installCourtFixtures(
notifications?: Record[];
onCapability?: (url: URL) => void;
onCommand?: (command: Record) => void;
+ reasonCapabilities?: Record[];
reportDetail?: Record;
reports?: Record[];
reportsStatus?: "available" | "unavailable";
@@ -333,7 +334,7 @@ async function installCourtFixtures(
affectedId: "hmrCurrentReporter",
affectedIdSource: "direct_reporter",
},
- reasonCapabilities: [
+ reasonCapabilities: options.reasonCapabilities ?? [
{
reason: { offenseCode: "GOV-03", lane: "court_report" },
standing: {
@@ -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,
@@ -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,
@@ -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",
diff --git a/tests/unit/court-presentation.test.ts b/tests/unit/court-presentation.test.ts
index 8451441..ac1f602 100644
--- a/tests/unit/court-presentation.test.ts
+++ b/tests/unit/court-presentation.test.ts
@@ -13,8 +13,10 @@ import {
courtRemedyLabel,
courtReportLaneChoiceLabel,
courtReportActionProgress,
+ courtReportProcessContext,
courtReportRouteDescription,
courtReportStateDisplay,
+ courtReportTriggerRequirement,
courtRemedyExpiry,
courtEventDisplay,
formatCourtDuration,
@@ -185,6 +187,83 @@ test("report route copy distinguishes non-case correction thresholds from Court
);
});
+test("safety intake never presents Governor reports as its Court trigger", () => {
+ assert.deepEqual(
+ courtReportStateDisplay("grouped", "safety_or_protocol_incident"),
+ {
+ 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.",
+ },
+ );
+ const process = courtReportProcessContext({
+ caseId: null,
+ lane: "safety_or_protocol_incident",
+ state: "grouped",
+ standing: {
+ status: "verified",
+ direct: true,
+ source: "affected_party",
+ },
+ });
+ assert.equal(process.destination, "Safety and protocol intake");
+ assert.match(process.nextStep, /objective proof.*emergency referral/i);
+ assert.doesNotMatch(process.nextStep, /Governor threshold/i);
+ assert.deepEqual(
+ courtReportTriggerRequirement(
+ "safety_or_protocol_incident",
+ { directStanding: true },
+ { communityThreshold: 10, viewerCountsTowardCommunity: true },
+ ),
+ {
+ 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.",
+ },
+ );
+});
+
+test("creation requirements follow correction, moderation, and Court trigger rules", () => {
+ const population = {
+ communityThreshold: 7,
+ viewerCountsTowardCommunity: true,
+ };
+ assert.deepEqual(
+ courtReportTriggerRequirement(
+ "correction",
+ { directStanding: true },
+ population,
+ ),
+ { kind: "community", required: 7, viewerCounts: true },
+ );
+ assert.equal(
+ courtReportTriggerRequirement(
+ "scoped_moderation",
+ { directStanding: false },
+ population,
+ ).kind,
+ "single",
+ );
+ assert.equal(
+ courtReportTriggerRequirement(
+ "court_report",
+ { directStanding: true },
+ population,
+ ).kind,
+ "single",
+ );
+ assert.deepEqual(
+ courtReportTriggerRequirement(
+ "court_report",
+ { directStanding: false },
+ population,
+ ),
+ { kind: "community", required: 7, viewerCounts: true },
+ );
+});
+
test("long audit values stay readable while preserving their identifying ends", () => {
const digest = `sha256:${"a".repeat(64)}`;
const compact = compactCourtAuditValue(digest);