diff --git a/docs/architecture-hardening-backlog.md b/docs/architecture-hardening-backlog.md index a65a97a..d2b4177 100644 --- a/docs/architecture-hardening-backlog.md +++ b/docs/architecture-hardening-backlog.md @@ -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 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 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 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. -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 diff --git a/packages/api/src/router/assistant-tools.ts b/packages/api/src/router/assistant-tools.ts index c85d513..24e2ffd 100644 --- a/packages/api/src/router/assistant-tools.ts +++ b/packages/api/src/router/assistant-tools.ts @@ -132,6 +132,10 @@ import { commentReadToolDefinitions, createCommentExecutors, } from "./assistant-tools/comments.js"; +import { + auditHistoryToolDefinitions, + createAuditHistoryExecutors, +} from "./assistant-tools/audit-history.js"; import { withToolAccess, type ToolAccessRequirements, @@ -425,8 +429,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial, ctx: ToolContext) { const caller = createImportExportCaller(createScopedCallerContext(ctx)); const csv = await caller.exportResourcesCSV(); diff --git a/packages/api/src/router/assistant-tools/audit-history.ts b/packages/api/src/router/assistant-tools/audit-history.ts new file mode 100644 index 0000000..f8ca4e8 --- /dev/null +++ b/packages/api/src/router/assistant-tools/audit-history.ts @@ -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; + }; + 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 { + 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, + }); + }, + }; +}