refactor(api): extract assistant audit-history slice

This commit is contained in:
2026-03-30 22:30:51 +02:00
parent ab32c7804b
commit d55ab67e04
3 changed files with 160 additions and 88 deletions
+2 -1
View File
@@ -41,12 +41,13 @@
- the blueprint and rate-card read helpers now live in their own domain module, keeping reference-data and pricing lookups out of the monolithic assistant router without changing the assistant contract - the blueprint and rate-card read helpers now live in their own domain module, keeping reference-data and pricing lookups out of the monolithic assistant router without changing the assistant contract
- the dashboard detail, insight summary, anomaly detection, and dynamic report read helpers now live in their own domain module, keeping controller-side analytics reads out of the monolithic assistant router without changing the assistant contract - the dashboard detail, insight summary, anomaly detection, and dynamic report read helpers now live in their own domain module, keeping controller-side analytics reads out of the monolithic assistant router without changing the assistant contract
- the comment listing and comment mutation assistant helpers now live in their own domain module, keeping collaboration-side comment flows out of the monolithic assistant router without changing the assistant contract - the comment listing and comment mutation assistant helpers now live in their own domain module, keeping collaboration-side comment flows out of the monolithic assistant router without changing the assistant contract
- the audit-history assistant helpers now live in their own domain module, keeping controller-side change-history reads out of the monolithic assistant router without changing the assistant contract
## Next Up ## Next Up
Pin the next structural cleanup on the API side: Pin the next structural cleanup on the API side:
continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract. continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract.
The next clean slice should stay adjacent to the extracted domains and target one cohesive analytics or audit block such as `query_change_history` / `get_entity_timeline` or the remaining scenario/narrative/rate-analysis helpers still living in the monolithic router. The next clean slice should stay adjacent to the extracted domains and target one cohesive analytics block such as the remaining scenario/narrative/rate-analysis helpers still living in the monolithic router.
## Remaining Major Themes ## Remaining Major Themes
+9 -87
View File
@@ -132,6 +132,10 @@ import {
commentReadToolDefinitions, commentReadToolDefinitions,
createCommentExecutors, createCommentExecutors,
} from "./assistant-tools/comments.js"; } from "./assistant-tools/comments.js";
import {
auditHistoryToolDefinitions,
createAuditHistoryExecutors,
} from "./assistant-tools/audit-history.js";
import { import {
withToolAccess, withToolAccess,
type ToolAccessRequirements, type ToolAccessRequirements,
@@ -425,8 +429,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequiremen
lookup_rate: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, lookup_rate: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
simulate_scenario: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, simulate_scenario: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
generate_project_narrative: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, generate_project_narrative: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
query_change_history: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
get_entity_timeline: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
export_resources_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, export_resources_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
export_projects_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, export_projects_csv: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
import_csv_data: { import_csv_data: {
@@ -2387,40 +2389,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
}, },
}, },
...commentMutationToolDefinitions, ...commentMutationToolDefinitions,
{ ...auditHistoryToolDefinitions,
type: "function",
function: {
name: "query_change_history",
description: "Search the audit history for changes to projects, resources, allocations, vacations, or any entity. Reuses the real audit log list API. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Filter by entity type (e.g. 'Project', 'Resource', 'Allocation', 'Vacation', 'Role', 'Estimate')" },
search: { type: "string", description: "Search in entity name or summary text" },
userId: { type: "string", description: "Filter by user ID who made the change" },
daysBack: { type: "integer", description: "How many days back to search. Default: 7" },
action: { type: "string", description: "Filter by action type: CREATE, UPDATE, DELETE, SHIFT, IMPORT" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_entity_timeline",
description: "Get the audit history for a specific entity (project, resource, etc.) via the real audit API. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Entity type (e.g. 'Project', 'Resource', 'Allocation')" },
entityId: { type: "string", description: "Entity ID" },
limit: { type: "integer", description: "Max results. Default: 50" },
},
required: ["entityType", "entityId"],
},
},
},
{ {
type: "function", type: "function",
function: { function: {
@@ -2824,6 +2793,10 @@ const executors = {
toAssistantCommentCreationError, toAssistantCommentCreationError,
toAssistantCommentResolveError, toAssistantCommentResolveError,
}), }),
...createAuditHistoryExecutors({
createAuditLogCaller,
createScopedCallerContext,
}),
async search_estimates(params: { async search_estimates(params: {
projectCode?: string; query?: string; status?: string; limit?: number; projectCode?: string; query?: string; status?: string; limit?: number;
@@ -3364,57 +3337,6 @@ const executors = {
return caller.generateProjectNarrative({ projectId: params.projectId }); return caller.generateProjectNarrative({ projectId: params.projectId });
}, },
async query_change_history(params: {
entityType?: string;
search?: string;
userId?: string;
daysBack?: number;
action?: string;
limit?: number;
}, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 20, 50);
const daysBack = params.daysBack ?? 7;
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
const result = await caller.listDetail({
...(params.entityType ? { entityType: params.entityType } : {}),
...(params.userId ? { userId: params.userId } : {}),
...(params.action ? { action: params.action } : {}),
...(params.search ? { search: params.search } : {}),
startDate,
limit,
});
return {
filters: {
entityType: params.entityType ?? null,
userId: params.userId ?? null,
action: params.action ?? null,
search: params.search ?? null,
daysBack,
},
itemCount: result.items.length,
nextCursor: result.nextCursor ?? null,
items: result.items,
};
},
async get_entity_timeline(params: {
entityType: string;
entityId: string;
limit?: number;
}, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 50, 200);
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
return caller.getByEntityDetail({
entityType: params.entityType,
entityId: params.entityId,
limit,
});
},
async export_resources_csv(_params: Record<string, never>, ctx: ToolContext) { async export_resources_csv(_params: Record<string, never>, ctx: ToolContext) {
const caller = createImportExportCaller(createScopedCallerContext(ctx)); const caller = createImportExportCaller(createScopedCallerContext(ctx));
const csv = await caller.exportResourcesCSV(); const csv = await caller.exportResourcesCSV();
@@ -0,0 +1,149 @@
import { SystemRole } from "@capakraken/shared";
import type { TRPCContext } from "../../trpc.js";
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
type AuditListItem = {
id: string;
entityType: string;
entityId: string;
entityName: string | null;
action: string;
userId: string | null;
source: string | null;
summary: string | null;
createdAt: string;
user: {
id: string;
name: string | null;
email: string | null;
} | null;
};
type AuditHistoryDeps = {
createAuditLogCaller: (ctx: TRPCContext) => {
listDetail: (params: {
entityType?: string;
entityId?: string;
userId?: string;
action?: string;
source?: string;
startDate?: Date;
endDate?: Date;
search?: string;
limit: number;
cursor?: string;
}) => Promise<{
items: AuditListItem[];
nextCursor: string | null;
}>;
getByEntityDetail: (params: {
entityType: string;
entityId: string;
limit: number;
}) => Promise<unknown>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
};
export const auditHistoryToolDefinitions: ToolDef[] = withToolAccess([
{
type: "function",
function: {
name: "query_change_history",
description: "Search the audit history for changes to projects, resources, allocations, vacations, or any entity. Reuses the real audit log list API. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Filter by entity type (e.g. 'Project', 'Resource', 'Allocation', 'Vacation', 'Role', 'Estimate')" },
search: { type: "string", description: "Search in entity name or summary text" },
userId: { type: "string", description: "Filter by user ID who made the change" },
daysBack: { type: "integer", description: "How many days back to search. Default: 7" },
action: { type: "string", description: "Filter by action type: CREATE, UPDATE, DELETE, SHIFT, IMPORT" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_entity_timeline",
description: "Get the audit history for a specific entity (project, resource, etc.) via the real audit API. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Entity type (e.g. 'Project', 'Resource', 'Allocation')" },
entityId: { type: "string", description: "Entity ID" },
limit: { type: "integer", description: "Max results. Default: 50" },
},
required: ["entityType", "entityId"],
},
},
},
], {
query_change_history: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
get_entity_timeline: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
});
export function createAuditHistoryExecutors(
deps: AuditHistoryDeps,
): Record<string, ToolExecutor> {
return {
async query_change_history(
params: {
entityType?: string;
search?: string;
userId?: string;
daysBack?: number;
action?: string;
limit?: number;
},
ctx: ToolContext,
) {
const limit = Math.min(params.limit ?? 20, 50);
const daysBack = params.daysBack ?? 7;
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
const caller = deps.createAuditLogCaller(deps.createScopedCallerContext(ctx));
const result = await caller.listDetail({
...(params.entityType ? { entityType: params.entityType } : {}),
...(params.userId ? { userId: params.userId } : {}),
...(params.action ? { action: params.action } : {}),
...(params.search ? { search: params.search } : {}),
startDate,
limit,
});
return {
filters: {
entityType: params.entityType ?? null,
userId: params.userId ?? null,
action: params.action ?? null,
search: params.search ?? null,
daysBack,
},
itemCount: result.items.length,
nextCursor: result.nextCursor ?? null,
items: result.items,
};
},
async get_entity_timeline(
params: { entityType: string; entityId: string; limit?: number },
ctx: ToolContext,
) {
const limit = Math.min(params.limit ?? 50, 200);
const caller = deps.createAuditLogCaller(deps.createScopedCallerContext(ctx));
return caller.getByEntityDetail({
entityType: params.entityType,
entityId: params.entityId,
limit,
});
},
};
}