Merge branch 'fix/docai-governance-rebased' into release/v1.27.31

This commit is contained in:
Marc-André Bombeck committed 2026-07-09 01:28:07 +02:00
commit 7b1f077888
31 files changed
+786 -100

No files matched your search

@@ -34,6 +34,7 @@ function base(
aiEnabled: true,
indexEnabled: true,
unavailableReason: null,
egressExternal: false,
actionsDisabled: false,
onSuggest: noop,
suggestPending: false,
@@ -142,6 +143,22 @@ describe("<DocumentAiSection>", () => {
expect(html).toContain("Not saved");
});
it("shows the vendor-blind egress notice before an external document read", () => {
const html = render(base({ aiEnabled: true, egressExternal: true }));
expect(html).toContain('data-slot="document-ai-egress-notice"');
expect(html).toContain("third-party AI service");
// Vendor-blind: never names a specific AI vendor in the document surface.
expect(html).not.toContain("OpenAI");
expect(html).not.toContain("ChatGPT");
// The actions are still offered — the notice informs, it does not block.
expect(html).toContain('data-slot="assist-suggest"');
});
it("omits the egress notice when the read stays on the local machine", () => {
const html = render(base({ aiEnabled: true, egressExternal: false }));
expect(html).not.toContain('data-slot="document-ai-egress-notice"');
});
it("hides the Read-with-AI action when indexing is unavailable but keeps suggest", () => {
const html = render(base({ aiEnabled: true, indexEnabled: false }));
expect(html).toContain('data-slot="assist-suggest"');
@@ -22,7 +22,7 @@
* status pill stays, honestly reflecting any local auto-index, and the manual
* form below stays fully usable.
*/
import { FileText, ScanText, WandSparkles } from "lucide-react";
import { FileText, ScanText, ShieldAlert, WandSparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
@@ -46,6 +46,7 @@ export function DocumentAiSection({
aiEnabled,
indexEnabled,
unavailableReason,
egressExternal,
actionsDisabled,
onSuggest,
suggestPending,
@@ -72,6 +73,12 @@ export function DocumentAiSection({
aiEnabled: boolean;
indexEnabled: boolean;
unavailableReason: "no-provider" | "enable-local-ocr" | null;
/**
* True when a document read will egress to a third-party AI service (any
* provider other than a self-hosted local model). Drives the vendor-blind
* "this leaves your machine" notice shown before the AI actions.
*/
egressExternal: boolean;
/** Capability still resolving — the transport mode isn't known yet. */
actionsDisabled: boolean;
onSuggest: () => void;
@@ -126,6 +133,17 @@ export function DocumentAiSection({
/>
</div>
{egressExternal ? (
<div
data-slot="document-ai-egress-notice"
role="note"
className="border-border text-muted-foreground flex items-start gap-2 rounded-lg border border-dashed px-3 py-2.5 text-xs"
>
<ShieldAlert className="mt-0.5 size-3.5 shrink-0" aria-hidden />
<p className="min-w-0">{t("documents.ai.egressNotice")}</p>
</div>
) : null}
{aiEnabled ? (
<>
<p className="text-muted-foreground text-xs">
@@ -505,6 +505,7 @@ export function DocumentDetailSheet({
aiEnabled={aiEnabled}
indexEnabled={indexEnabled}
unavailableReason={capability.data?.reason ?? null}
egressExternal={capability.data?.egress === "external"}
actionsDisabled={capability.isPending}
onSuggest={runSuggest}
suggestPending={suggest.isPending}
@@ -17,8 +17,8 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { apiGet, ApiError } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
import type { OcrCapabilityDto } from "@/lib/validations/labs-ocr";
import type {
DocumentAiCapabilityDto,
DocumentSuggestionDto,
DocumentSummaryMode,
} from "@/lib/validations/inbound-documents";
@@ -34,16 +34,18 @@ import {
export type DocumentDescribeResult = { summary: string } | { text: string };
/**
* The shared OCR capability probe (`/api/labs/ocr/capability`) reused verbatim:
* the same server resolution both the lab-OCR and the document AI routes gate
* on, so the client picks the transport the endpoint accepts. Availability of
* the affordance itself is gated on `usage.assistAvailable`; this read only
* resolves the transport `mode` + PDF support.
* The document-scoped AI capability probe (`/api/documents/inbound/capability`).
* Resolved over the DOCUMENT provider order (local-first, codex last), so the
* transport `mode` + PDF support match what the document routes do, and it
* carries the vendor-blind `egress` class the detail sheet uses to warn before a
* read leaves the machine. Availability of the affordance itself is still gated
* on `usage.assistAvailable`.
*/
export function useDocumentAiCapability(enabled: boolean) {
return useQuery<OcrCapabilityDto>({
queryKey: queryKeys.ocrCapability(),
queryFn: () => apiGet<OcrCapabilityDto>("/api/labs/ocr/capability"),
return useQuery<DocumentAiCapabilityDto>({
queryKey: queryKeys.inboundDocumentAiCapability(),
queryFn: () =>
apiGet<DocumentAiCapabilityDto>("/api/documents/inbound/capability"),
enabled,
staleTime: 60_000,
});