feat(documents): expose content-index provenance on the document DTO

Surface the content-search index's source (vision vs local extraction) on
the list + detail DTO so the vault UI can tell an AI-read document from a
locally-indexed one. Threads the source through both read paths and the
serialisers, regenerates the OpenAPI contract, and adds a narrowing helper
for the free-String column.
This commit is contained in:
Marc-André Bombeck committed 2026-07-08 21:15:52 +02:00
1 parent d7ff74bb2e
commit bb0ebb1dce
9 files changed
+108 -14

No files matched your search

+15 -3
View File
@@ -27,7 +27,10 @@ import { serialiseDocumentDetail } from "@/lib/documents/store";
import { annotate } from "@/lib/logging/context";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { documentUpdateSchema } from "@/lib/validations/inbound-documents";
import {
documentUpdateSchema,
toContentIndexSource,
} from "@/lib/validations/inbound-documents";
import type { Prisma } from "@/generated/prisma/client";
type RouteParams = { params: Promise<{ id: string }> };
@@ -80,7 +83,7 @@ export const GET = apiHandler(
loadConditionLinks(user.id, [document.id]),
prisma.documentContentIndex.findUnique({
where: { documentId: document.id },
select: { id: true },
select: { source: true },
}),
]);
@@ -95,6 +98,7 @@ export const GET = apiHandler(
document.facts,
links.get(document.id) ?? [],
contentIndex !== null,
toContentIndexSource(contentIndex?.source),
),
);
},
@@ -173,7 +177,13 @@ export const PATCH = apiHandler(
omit: { contentEncrypted: true },
include: { facts: { orderBy: { createdAt: "asc" } } },
});
const links = await loadConditionLinks(user.id, [document.id]);
const [links, contentIndex] = await Promise.all([
loadConditionLinks(user.id, [document.id]),
prisma.documentContentIndex.findUnique({
where: { documentId: document.id },
select: { source: true },
}),
]);
const touched = [
...Object.keys(data),
@@ -199,6 +209,8 @@ export const PATCH = apiHandler(
document,
document.facts,
links.get(document.id) ?? [],
contentIndex !== null,
toContentIndexSource(contentIndex?.source),
),
);
},
+9 -6
View File
@@ -55,6 +55,7 @@ import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import {
documentCreateSchema,
documentListQuerySchema,
toContentIndexSource,
} from "@/lib/validations/inbound-documents";
import type { Prisma } from "@/generated/prisma/client";
@@ -506,15 +507,16 @@ export const GET = apiHandler(async (request: Request) => {
page.map((d) => d.id),
);
// Which of the page's documents have a content index (drives `hasContentIndex`
// + the re-index affordance). One indexed grouped query; never the ciphertext.
const indexedIds = new Set<string>();
// Which of the page's documents have a content index (drives the searchable
// status + the provenance the UI reads to tell an AI-read document from a
// locally-indexed one). One grouped query; never the ciphertext.
const indexSources = new Map<string, string>();
if (page.length > 0) {
const indexed = await prisma.documentContentIndex.findMany({
where: { userId: user.id, documentId: { in: page.map((d) => d.id) } },
select: { documentId: true },
select: { documentId: true, source: true },
});
for (const row of indexed) indexedIds.add(row.documentId);
for (const row of indexed) indexSources.set(row.documentId, row.source);
}
annotate({
@@ -533,7 +535,8 @@ export const GET = apiHandler(async (request: Request) => {
doc,
factCounts.get(doc.id) ?? { factCount: 0, pendingCount: 0 },
linkMap.get(doc.id) ?? [],
indexedIds.has(doc.id),
indexSources.has(doc.id),
toContentIndexSource(indexSources.get(doc.id)),
),
),
nextCursor,