perf(bundle): keep zod out of the documents and Coach client routes

Both routes reached the schema library through a constant: the list of
document kinds, the list and bulk limits, the attachment cap, the Coach
data clusters and preference defaults, the conversation title length,
the scope source list and the report leaf order. Each sat in a schema
module, so importing the number shipped the library. The constants now
live in zod-free modules (lib/documents/inbound-constants,
lib/ai/coach/prefs-constants, lib/ai/coach/scope-sources,
lib/report-selection/catalogue) and the schema modules re-export them
for server code; the Coach page checks its scope parameter against the
closed list instead of parsing it. A guard walks each route's static
client graph with the TypeScript parser and fails on a zod import.

Two guards follow earlier commits in this branch: the fetch-limit
allowlist names the shared document picker sheet, and the card density
test states a floor on the cards it finds.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 14:04:06 +02:00
1 parent 3bae93256d
commit d0677adac9
26 files changed
+421 -190

No files matched your search

@@ -36,13 +36,13 @@ import { Button } from "@/components/ui/button";
import type { AiRunPhase } from "@/hooks/use-ai-run";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import {
isAiReadSource,
type DocumentContentIndexSourceValue,
type DocumentIndexOutcomeValue,
type DocumentSuggestionDto,
type DocumentSummaryMode,
import type {
DocumentContentIndexSourceValue,
DocumentIndexOutcomeValue,
DocumentSuggestionDto,
DocumentSummaryMode,
} from "@/lib/validations/inbound-documents";
import { isAiReadSource } from "@/lib/documents/inbound-constants";
import {
AiRunPhaseNote,
@@ -75,14 +75,14 @@ import {
useFencedObjectUrl,
} from "@/hooks/use-fenced-object-url";
import { cn } from "@/lib/utils";
import {
INBOUND_DOCUMENT_KINDS,
type DocumentSourceSystemValue,
type DocumentSuggestionDto,
type DocumentSummaryMode,
type InboundDocumentDetailDto,
type InboundDocumentKindValue,
import type {
DocumentSourceSystemValue,
DocumentSuggestionDto,
DocumentSummaryMode,
InboundDocumentDetailDto,
InboundDocumentKindValue,
} from "@/lib/validations/inbound-documents";
import { INBOUND_DOCUMENT_KINDS } from "@/lib/documents/inbound-constants";
import { DocumentAiSection } from "./document-ai-section";
import { DocumentEncounterSuggestion } from "./document-encounter-suggestion";
import { DocumentRecordLinks } from "./document-record-links";
+8 -8
View File
@@ -61,15 +61,15 @@ import {
} from "@/lib/insights/coach-launch-context";
import { invalidateKeys, queryKeys } from "@/lib/query-keys";
import { useDocumentsLayout } from "@/lib/queries/use-documents-layout";
import {
DOCUMENT_BULK_MAX_IDS,
type DocumentBulkAction,
type DocumentBulkResultDto,
type DocumentUsageDto,
type InboundDocumentDetailDto,
type InboundDocumentDto,
type InboundDocumentKindValue,
import type {
DocumentBulkAction,
DocumentBulkResultDto,
DocumentUsageDto,
InboundDocumentDetailDto,
InboundDocumentDto,
InboundDocumentKindValue,
} from "@/lib/validations/inbound-documents";
import { DOCUMENT_BULK_MAX_IDS } from "@/lib/documents/inbound-constants";
import { DocumentBulkBar } from "./document-bulk-bar";
import { DocumentDetailSheet } from "./document-detail-sheet";
import { DocumentShareSheet } from "./document-share-sheet";
+6 -4
View File
@@ -7,13 +7,15 @@
* and byte formatting. Kept out of the client components so they stay
* trivially unit-testable without a render harness.
*/
import type {
DocumentLinkedProcedureDto,
InboundDocumentDto,
InboundDocumentKindValue,
} from "@/lib/validations/inbound-documents";
import {
DOCUMENT_LIST_DEFAULT_LIMIT,
INBOUND_DOCUMENT_KINDS,
type DocumentLinkedProcedureDto,
type InboundDocumentDto,
type InboundDocumentKindValue,
} from "@/lib/validations/inbound-documents";
} from "@/lib/documents/inbound-constants";
import type { DocumentVaultFilters } from "@/lib/query-keys/documents";
import { userDayKey } from "@/lib/tz/format";
@@ -19,7 +19,7 @@ import { apiDelete, apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
import type { CoachFollowUp, CoachScope } from "@/lib/ai/coach/types";
import type { CoachLaunchScope } from "@/lib/insights/coach-launch-context";
import type { InboundDocumentDetailDto } from "@/lib/validations/inbound-documents";
import { MAX_COACH_ATTACHMENTS } from "@/lib/validations/inbound-documents";
import { MAX_COACH_ATTACHMENTS } from "@/lib/documents/inbound-constants";
import { useModuleEnabled } from "@/hooks/use-module-enabled";
import {
parseUploadResponse,
@@ -38,7 +38,7 @@ import {
type ReasoningLevel,
} from "@/lib/ai/reasoning/levels";
import type { CoachReasoningState } from "@/lib/ai/reasoning/resolve";
import { coachReasoningLevel } from "@/lib/validations/coach-prefs";
import { coachReasoningLevel } from "@/lib/ai/coach/prefs-constants";
export type { CoachReasoningState };
@@ -6,7 +6,7 @@ import { Check, Loader2, Pencil, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { COACH_CONVERSATION_TITLE_MAX } from "@/lib/ai/coach/types";
import { COACH_CONVERSATION_TITLE_MAX } from "@/lib/ai/coach/scope-sources";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
@@ -22,13 +22,15 @@ import {
HISTORY_REACH_DAYS,
lookbackText,
} from "@/lib/ai/coach/history-reach";
import type {
CoachDefaultWindow,
CoachPrefs,
} from "@/lib/validations/coach-prefs";
import {
DEFAULT_COACH_CLUSTERS,
coachDataClusterEnum,
COACH_DATA_CLUSTERS,
type CoachDataCluster,
type CoachDefaultWindow,
type CoachPrefs,
} from "@/lib/validations/coach-prefs";
} from "@/lib/ai/coach/prefs-constants";
/**
* v1.7.2 — "What I can see" rail, backed by the persisted Coach prefs.
@@ -48,8 +50,7 @@ import {
* member count beneath each label spells out the scope so the user can
* see how much data each toggle pulls in.
*/
const CLUSTER_OPTIONS: ReadonlyArray<CoachDataCluster> =
coachDataClusterEnum.options;
const CLUSTER_OPTIONS: ReadonlyArray<CoachDataCluster> = COACH_DATA_CLUSTERS;
/**
* The lookback options, narrowest first. The choice is a limit
@@ -46,16 +46,18 @@ import {
import { useAiCapability, useCoachReasoning } from "@/hooks/use-ai-capability";
import { COACH_FOLLOW_UP_UI_KEYS } from "@/lib/ai/coach/dialog-keys";
import { HISTORY_REACH_DAYS, lookbackText } from "@/lib/ai/coach/history-reach";
import type {
CoachExcludeMetric,
CoachPrefs,
CoachTone,
CoachVerbosity,
} from "@/lib/validations/coach-prefs";
import {
DEFAULT_COACH_CLUSTERS,
DEFAULT_COACH_PREFS,
coachDataClusterEnum,
COACH_DATA_CLUSTERS,
coachReasoningLevel,
type CoachExcludeMetric,
type CoachPrefs,
type CoachTone,
type CoachVerbosity,
} from "@/lib/validations/coach-prefs";
} from "@/lib/ai/coach/prefs-constants";
const EXCLUDE_OPTIONS: CoachExcludeMetric[] = [
"bp",
@@ -89,7 +91,7 @@ function sourcesSummary(
): string {
const params = {
count: (prefs.dataClusters ?? DEFAULT_COACH_CLUSTERS).length,
total: coachDataClusterEnum.options.length,
total: COACH_DATA_CLUSTERS.length,
};
const window = prefs.defaultWindow;
if (HISTORY_REACH_DAYS[window] === null) {
@@ -63,7 +63,7 @@ import {
parseSavedProfile,
SAVED_PROFILE_FALLBACK,
} from "@/lib/report-selection/profile-shape";
import { orderLeaves } from "@/lib/report-selection/selection";
import { orderLeaves } from "@/lib/report-selection/catalogue";
import { ReportScopePicker } from "./report-scope-picker";
import { ScopeSummary } from "./scope-summary";
@@ -36,7 +36,7 @@ import {
import { ReportScopePicker } from "@/components/settings/report-selection/report-scope-picker";
import { ScopeSummary } from "@/components/settings/report-selection/scope-summary";
import type { ReportLeafId } from "@/lib/report-selection/catalogue";
import { orderLeaves } from "@/lib/report-selection/selection";
import { orderLeaves } from "@/lib/report-selection/catalogue";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { apiPost } from "@/lib/api/api-fetch";
@@ -44,5 +44,13 @@ describe("compact density is the prop, not a class list", () => {
),
);
expect(hits).toEqual([]);
// The prop is where the compact cards went; a walk that finds none of
// them read the wrong tree.
const users = walkSourceFiles(src, { floor: 2500 }).filter(
(p) =>
p.endsWith(".tsx") &&
readFileSync(join(src, p), "utf8").includes('density="compact"'),
);
expect(users.length).toBeGreaterThanOrEqual(20);
});
});