refactor(api): extract assistant blueprint rate-card slice

This commit is contained in:
2026-03-30 22:17:41 +02:00
parent e1496064e0
commit 6c6afdd059
3 changed files with 223 additions and 130 deletions
+2 -1
View File
@@ -38,12 +38,13 @@
- the project search, lifecycle, and cover-art helpers now live in their own domain module, keeping project orchestration out of the monolithic assistant router without changing the assistant contract - the project search, lifecycle, and cover-art helpers now live in their own domain module, keeping project orchestration out of the monolithic assistant router without changing the assistant contract
- the demand, staffing-suggestion, capacity, and resource-availability assistant helpers now live in their own domain module, keeping staffing orchestration out of the monolithic assistant router without changing the assistant contract - the demand, staffing-suggestion, capacity, and resource-availability assistant helpers now live in their own domain module, keeping staffing orchestration out of the monolithic assistant router without changing the assistant contract
- the resource search, detail, and lifecycle assistant helpers now live in their own domain module, keeping resource CRUD orchestration out of the monolithic assistant router without changing the assistant contract - the resource search, detail, and lifecycle assistant helpers now live in their own domain module, keeping resource CRUD orchestration 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
## 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 block such as blueprint/rate-card/reference-data reads or another tightly bound CRUD/read-model cluster still in the monolithic router. The next clean slice should stay adjacent to the extracted domains and target one cohesive read-model block such as dashboard/insight/report helpers or another tightly bound cluster still in the monolithic router.
## Remaining Major Themes ## Remaining Major Themes
+14 -129
View File
@@ -121,6 +121,10 @@ import {
resourceMutationToolDefinitions, resourceMutationToolDefinitions,
resourceReadToolDefinitions, resourceReadToolDefinitions,
} from "./assistant-tools/resources.js"; } from "./assistant-tools/resources.js";
import {
blueprintsRateCardsToolDefinitions,
createBlueprintsRateCardsExecutors,
} from "./assistant-tools/blueprints-rate-cards.js";
import { import {
withToolAccess, withToolAccess,
type ToolAccessRequirements, type ToolAccessRequirements,
@@ -408,16 +412,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequiremen
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] }, get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] }, get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] }, set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
list_blueprints: { requiresPlanningRead: true },
get_blueprint: { requiresPlanningRead: true },
list_rate_cards: {
allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES],
requiresCostView: true,
},
resolve_rate: {
allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES],
requiresCostView: true,
},
get_country: { requiresResourceOverview: true }, get_country: { requiresResourceOverview: true },
get_dashboard_detail: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, get_dashboard_detail: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
@@ -2275,59 +2269,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
...staffingDemandMutationToolDefinitions, ...staffingDemandMutationToolDefinitions,
// ── BLUEPRINT ── // ── BLUEPRINT ──
{ ...blueprintsRateCardsToolDefinitions,
type: "function",
function: {
name: "list_blueprints",
description: "List available project blueprints with their field definitions.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_blueprint",
description: "Get detailed blueprint with all field definitions and role presets.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Blueprint ID or name (partial match)" },
},
required: ["identifier"],
},
},
},
// ── RATE CARDS ──
{
type: "function",
function: {
name: "list_rate_cards",
description: "List rate cards with their effective dates and line items.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search by name" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "resolve_rate",
description: "Look up the applicable rate for a resource, role, or management level from rate cards.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID or name" },
roleName: { type: "string", description: "Role name" },
date: { type: "string", description: "Date to check rate for (YYYY-MM-DD). Default: today" },
},
},
},
},
// ── ESTIMATES ── // ── ESTIMATES ──
...estimateReadToolDefinitions, ...estimateReadToolDefinitions,
@@ -3020,6 +2962,15 @@ const executors = {
resolveResourceIdentifier, resolveResourceIdentifier,
resolveProjectIdentifier, resolveProjectIdentifier,
}), }),
...createBlueprintsRateCardsExecutors({
createBlueprintCaller,
createRateCardCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
parseOptionalIsoDate,
fmtDate,
}),
async search_estimates(params: { async search_estimates(params: {
projectCode?: string; query?: string; status?: string; limit?: number; projectCode?: string; query?: string; status?: string; limit?: number;
@@ -3388,72 +3339,6 @@ const executors = {
}; };
}, },
// ── BLUEPRINT ──
async list_blueprints(_params: Record<string, never>, ctx: ToolContext) {
const caller = createBlueprintCaller(createScopedCallerContext(ctx));
const blueprints = await caller.listSummaries();
return blueprints.map((b) => ({
id: b.id,
name: b.name,
projectCount: b._count.projects,
}));
},
async get_blueprint(params: { identifier: string }, ctx: ToolContext) {
const caller = createBlueprintCaller(createScopedCallerContext(ctx));
const bp = await resolveEntityOrAssistantError(
() => caller.getByIdentifier({ identifier: params.identifier }),
`Blueprint not found: ${params.identifier}`,
);
if ("error" in bp) {
return bp;
}
return {
id: bp.id,
name: bp.name,
fieldDefs: bp.fieldDefs,
rolePresets: bp.rolePresets,
};
},
// ── RATE CARDS ──
async list_rate_cards(params: { query?: string; limit?: number }, ctx: ToolContext) {
const caller = createRateCardCaller(createScopedCallerContext(ctx));
const cards = await caller.list({
isActive: true,
...(params.query ? { search: params.query } : {}),
});
return cards.map((c) => ({
id: c.id,
name: c.name,
effectiveFrom: fmtDate(c.effectiveFrom),
effectiveTo: fmtDate(c.effectiveTo),
lineCount: c._count.lines,
})).slice(0, Math.min(params.limit ?? 20, 50));
},
async resolve_rate(params: { resourceId?: string; roleName?: string; date?: string }, ctx: ToolContext) {
const caller = createRateCardCaller(createScopedCallerContext(ctx));
const date = parseOptionalIsoDate(params.date, "date");
if (params.resourceId) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
return caller.resolveBestRate({
resourceId: resource.id,
...(date ? { date } : {}),
});
}
return caller.resolveBestRate({
...(params.roleName ? { roleName: params.roleName } : {}),
...(date ? { date } : {}),
});
},
// ── ESTIMATES ── // ── ESTIMATES ──
...createEstimateExecutors({ ...createEstimateExecutors({
assertPermission, assertPermission,
@@ -0,0 +1,207 @@
import { SystemRole } from "@capakraken/shared";
import type { TRPCContext } from "../../trpc.js";
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
type AssistantToolErrorResult = { error: string };
type BlueprintRecord = {
id: string;
name: string;
fieldDefs: unknown;
rolePresets: unknown;
};
type ResolvedResource = {
id: string;
};
type BlueprintsRateCardsDeps = {
createBlueprintCaller: (ctx: TRPCContext) => {
listSummaries: () => Promise<Array<{
id: string;
name: string;
_count: { projects: number };
}>>;
getByIdentifier: (params: { identifier: string }) => Promise<BlueprintRecord>;
};
createRateCardCaller: (ctx: TRPCContext) => {
list: (params: {
isActive: boolean;
search?: string;
}) => Promise<Array<{
id: string;
name: string;
effectiveFrom: Date | null;
effectiveTo: Date | null;
_count: { lines: number };
}>>;
resolveBestRate: (params: {
resourceId?: string;
roleName?: string;
date?: Date;
}) => Promise<unknown>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
resolveResourceIdentifier: (
ctx: ToolContext,
identifier: string,
) => Promise<ResolvedResource | AssistantToolErrorResult>;
resolveEntityOrAssistantError: <T>(
resolve: () => Promise<T>,
notFoundMessage: string,
) => Promise<T | AssistantToolErrorResult>;
parseOptionalIsoDate: (
value: string | undefined,
fieldName: string,
) => Date | undefined;
fmtDate: (value: Date | null | undefined) => string | null;
};
export const blueprintsRateCardsToolDefinitions: ToolDef[] = withToolAccess([
{
type: "function",
function: {
name: "list_blueprints",
description: "List available project blueprints with their field definitions.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_blueprint",
description: "Get detailed blueprint with all field definitions and role presets.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Blueprint ID or name (partial match)" },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "list_rate_cards",
description: "List rate cards with their effective dates and line items.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search by name" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "resolve_rate",
description: "Look up the applicable rate for a resource, role, or management level from rate cards.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID or name" },
roleName: { type: "string", description: "Role name" },
date: { type: "string", description: "Date to check rate for (YYYY-MM-DD). Default: today" },
},
},
},
},
], {
list_blueprints: {
requiresPlanningRead: true,
},
get_blueprint: {
requiresPlanningRead: true,
},
list_rate_cards: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
requiresCostView: true,
},
resolve_rate: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
requiresCostView: true,
},
});
export function createBlueprintsRateCardsExecutors(
deps: BlueprintsRateCardsDeps,
): Record<string, ToolExecutor> {
return {
async list_blueprints(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createBlueprintCaller(deps.createScopedCallerContext(ctx));
const blueprints = await caller.listSummaries();
return blueprints.map((blueprint) => ({
id: blueprint.id,
name: blueprint.name,
projectCount: blueprint._count.projects,
}));
},
async get_blueprint(params: { identifier: string }, ctx: ToolContext) {
const caller = deps.createBlueprintCaller(deps.createScopedCallerContext(ctx));
const blueprint = await deps.resolveEntityOrAssistantError(
() => caller.getByIdentifier({ identifier: params.identifier }),
`Blueprint not found: ${params.identifier}`,
);
if ("error" in blueprint) {
return blueprint;
}
return {
id: blueprint.id,
name: blueprint.name,
fieldDefs: blueprint.fieldDefs,
rolePresets: blueprint.rolePresets,
};
},
async list_rate_cards(
params: { query?: string; limit?: number },
ctx: ToolContext,
) {
const caller = deps.createRateCardCaller(deps.createScopedCallerContext(ctx));
const cards = await caller.list({
isActive: true,
...(params.query ? { search: params.query } : {}),
});
return cards
.map((card) => ({
id: card.id,
name: card.name,
effectiveFrom: deps.fmtDate(card.effectiveFrom),
effectiveTo: deps.fmtDate(card.effectiveTo),
lineCount: card._count.lines,
}))
.slice(0, Math.min(params.limit ?? 20, 50));
},
async resolve_rate(
params: { resourceId?: string; roleName?: string; date?: string },
ctx: ToolContext,
) {
const caller = deps.createRateCardCaller(deps.createScopedCallerContext(ctx));
const date = deps.parseOptionalIsoDate(params.date, "date");
if (params.resourceId) {
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
return caller.resolveBestRate({
resourceId: resource.id,
...(date ? { date } : {}),
});
}
return caller.resolveBestRate({
...(params.roleName ? { roleName: params.roleName } : {}),
...(date ? { date } : {}),
});
},
};
}