feat: add governed cross-module reporting

This commit is contained in:
2026-08-02 05:29:38 +02:00
parent eba35edd3c
commit 0abcc2455e
21 changed files with 2497 additions and 38 deletions
+137
View File
@@ -119,6 +119,74 @@ export type ReportingSavedView = {
updated_at: string;
};
export type ProviderReportParameter = {
key: string;
label: string;
type: string;
required: boolean;
description?: string | null;
options_from_provider: boolean;
};
export type ProviderReportField = {
path: string;
label: string;
type: string;
group: string;
nullable: boolean;
sensitive: boolean;
};
export type ProviderReportDescriptor = {
contract_version: string;
provider_id: string;
report_id: string;
revision: string;
title: string;
summary: string;
parameters: ProviderReportParameter[];
result_schema: ProviderReportField[];
privacy_transforms: Array<{ id: string; label: string; required: boolean }>;
purpose_required: boolean;
audience_scope_required: boolean;
retention_class: string;
export_formats: Array<"json" | "csv">;
reidentification_risk: "low" | "moderate" | "high";
presentation: { kind?: string };
available: boolean;
unavailable_reason?: string | null;
governance: {
retention_days?: number | null;
export_formats: string[];
required_privacy_transforms: string[];
provenance: Record<string, unknown>;
};
};
export type ProviderReportExecution = {
execution_id: string;
provider_id: string;
report_id: string;
report_revision: string;
contract_version: string;
purpose: string;
audience_scope: Record<string, unknown>;
parameters: Record<string, unknown>;
result_schema: ProviderReportField[];
result: Record<string, unknown>;
source_revisions: Array<Record<string, unknown>>;
effective_scope: Record<string, unknown>;
privacy_transforms: string[];
provenance: Record<string, unknown>;
governance_provenance: Record<string, unknown>;
retention_class: string;
retention_days?: number | null;
expires_at?: string | null;
output_hash: string;
generated_at: string;
actor_id?: string | null;
};
export function listDefinitions(
settings: ApiSettings,
options: { kinds?: ReportingDefinitionKind[]; status?: string[]; query?: string; limit?: number },
@@ -132,6 +200,75 @@ export function listDefinitions(
}), { signal });
}
export function listProviderReports(
settings: ApiSettings,
signal?: AbortSignal
): Promise<{ reports: ProviderReportDescriptor[]; diagnostics: Array<Record<string, string>> }> {
return apiFetch(settings, "/api/v1/reporting/provider-reports", { signal });
}
export function listProviderParameterOptions(
settings: ApiSettings,
report: ProviderReportDescriptor,
parameterKey: string,
query = "",
signal?: AbortSignal
): Promise<{ options: Array<{ value: string; label: string; description?: string | null }> }> {
return apiFetch(settings, apiPath(
`/api/v1/reporting/provider-reports/${encodeURIComponent(report.provider_id)}/${encodeURIComponent(report.report_id)}/parameters/${encodeURIComponent(parameterKey)}/options`,
{ query, limit: 200 }
), { signal });
}
export function runProviderReport(
settings: ApiSettings,
report: ProviderReportDescriptor,
parameters: Record<string, unknown>,
purpose: string,
audienceScope: Record<string, unknown>
): Promise<ProviderReportExecution> {
return apiFetch(
settings,
`/api/v1/reporting/provider-reports/${encodeURIComponent(report.provider_id)}/${encodeURIComponent(report.report_id)}/executions`,
{
method: "POST",
body: JSON.stringify({
parameters,
purpose,
audience_scope: audienceScope,
idempotency_key: crypto.randomUUID()
})
}
);
}
export async function downloadProviderExecution(
settings: ApiSettings,
execution: ProviderReportExecution,
format: "json" | "csv",
purpose: string,
audienceScope: Record<string, unknown>
): Promise<void> {
const response = await fetch(
apiUrl(settings, `/api/v1/reporting/provider-executions/${encodeURIComponent(execution.execution_id)}/exports`),
{
method: "POST",
headers: { ...authHeaders(settings), "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ format, purpose, audience_scope: audienceScope })
}
);
if (!response.ok) throw new Error(await response.text());
const blob = await response.blob();
const disposition = response.headers.get("Content-Disposition") ?? "";
const filename = disposition.match(/filename="?([^";]+)"?/)?.[1] ?? `provider-report.${format}`;
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
export function getDefinition(
settings: ApiSettings,
kind: ReportingDefinitionKind,