diff --git a/docs/architecture-hardening-backlog.md b/docs/architecture-hardening-backlog.md index 621be7e..7c2abf2 100644 --- a/docs/architecture-hardening-backlog.md +++ b/docs/architecture-hardening-backlog.md @@ -30,12 +30,13 @@ - the neighboring client and org-unit admin mutations now live in their own domain module, keeping more CRUD wiring out of the monolithic router without changing the assistant contract - the adjacent chargeability/computation read helpers now live in their own domain module, keeping the advanced financial transparency read models out of the monolithic router without changing the assistant contract - the neighboring country and metro-city admin mutations now live in their own domain module, keeping more settings-side CRUD wiring out of the monolithic router without changing the assistant contract +- the adjacent management-level, utilization, calculation-rule, effort-rule, and experience-multiplier read helpers now live in their own domain module, further shrinking 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 block such as management-level plus utilization-rule configuration reads, or the remaining estimate and project admin helper clusters that are still embedded in the monolithic router. +The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the authenticated user/self-service assistant tools, or the remaining estimate and project admin helper clusters that are still embedded 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 3096074..aacdb19 100644 --- a/packages/api/src/router/assistant-tools.ts +++ b/packages/api/src/router/assistant-tools.ts @@ -87,6 +87,10 @@ import { chargeabilityComputationReadToolDefinitions, createChargeabilityComputationExecutors, } from "./assistant-tools/chargeability-computation.js"; +import { + configReadmodelToolDefinitions, + createConfigReadmodelExecutors, +} from "./assistant-tools/config-readmodels.js"; import { countryMetroAdminToolDefinitions, createCountryMetroAdminExecutors, @@ -2776,46 +2780,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [ }, }, ...countryMetroAdminToolDefinitions, - { - type: "function", - function: { - name: "list_management_levels", - description: "List management level groups and their levels with target percentages.", - parameters: { type: "object", properties: {} }, - }, - }, - { - type: "function", - function: { - name: "list_utilization_categories", - description: "List utilization categories (cost classification for projects).", - parameters: { type: "object", properties: {} }, - }, - }, - { - type: "function", - function: { - name: "list_calculation_rules", - description: "List calculation rules for cost attribution and chargeability.", - parameters: { type: "object", properties: {} }, - }, - }, - { - type: "function", - function: { - name: "list_effort_rules", - description: "List effort estimation rules with their formulas and conditions.", - parameters: { type: "object", properties: {} }, - }, - }, - { - type: "function", - function: { - name: "list_experience_multipliers", - description: "List experience multipliers that adjust effort estimates based on seniority.", - parameters: { type: "object", properties: {} }, - }, - }, + ...configReadmodelToolDefinitions, { type: "function", function: { @@ -5503,102 +5468,14 @@ const executors = { toAssistantCountryMutationError, toAssistantMetroCityMutationError, }), - - async list_management_levels(_params: Record, ctx: ToolContext) { - const caller = createManagementLevelCaller(createScopedCallerContext(ctx)); - const groups = await caller.listGroups(); - return groups.map((g) => ({ - id: g.id, - name: g.name, - target: g.targetPercentage ? `${g.targetPercentage}%` : null, - levels: g.levels.map((l: { id: string; name: string }) => ({ id: l.id, name: l.name })), - })); - }, - - async list_utilization_categories(_params: Record, ctx: ToolContext) { - const caller = createUtilizationCategoryCaller(createScopedCallerContext(ctx)); - const categories = await caller.list(); - const categoriesWithCounts = await Promise.all( - categories.map(async (category) => { - const detailed = await caller.getById({ id: category.id }); - return { - category, - projectCount: detailed._count.projects, - }; - }), - ); - return categoriesWithCounts.map(({ category, projectCount }) => ({ - id: category.id, - code: category.code, - name: category.name, - description: category.description, - projectCount, - })); - }, - - async list_calculation_rules(_params: Record, ctx: ToolContext) { - const caller = createCalculationRuleCaller(createScopedCallerContext(ctx)); - const rules = await caller.list(); - return rules.map((rule) => ({ - id: rule.id, - name: rule.name, - description: rule.description, - isActive: rule.isActive, - triggerType: rule.triggerType, - orderType: rule.orderType, - costEffect: rule.costEffect, - costReductionPercent: rule.costReductionPercent, - chargeabilityEffect: rule.chargeabilityEffect, - priority: rule.priority, - project: rule.project - ? { - id: rule.project.id, - name: rule.project.name, - shortCode: rule.project.shortCode, - } - : null, - })); - }, - - async list_effort_rules(_params: Record, ctx: ToolContext) { - const caller = createEffortRuleCaller(createScopedCallerContext(ctx)); - const ruleSets = await caller.list(); - return ruleSets.flatMap((ruleSet) => ruleSet.rules.map((rule) => ({ - id: rule.id, - description: rule.description, - scopeType: rule.scopeType, - discipline: rule.discipline, - chapter: rule.chapter, - unitMode: rule.unitMode, - hoursPerUnit: rule.hoursPerUnit, - sortOrder: rule.sortOrder, - ruleSet: { - name: ruleSet.name, - isDefault: ruleSet.isDefault, - }, - }))); - }, - - async list_experience_multipliers(_params: Record, ctx: ToolContext) { - const caller = createExperienceMultiplierCaller(createScopedCallerContext(ctx)); - const multiplierSets = await caller.list(); - return multiplierSets.flatMap((multiplierSet) => multiplierSet.rules.map((rule) => ({ - id: rule.id, - description: rule.description, - chapter: rule.chapter, - location: rule.location, - level: rule.level, - costMultiplier: rule.costMultiplier, - billMultiplier: rule.billMultiplier, - shoringRatio: rule.shoringRatio, - additionalEffortRatio: rule.additionalEffortRatio, - sortOrder: rule.sortOrder, - multiplierSet: { - name: multiplierSet.name, - isDefault: multiplierSet.isDefault, - }, - }))); - }, + ...createConfigReadmodelExecutors({ + createManagementLevelCaller, + createUtilizationCategoryCaller, + createCalculationRuleCaller, + createEffortRuleCaller, + createExperienceMultiplierCaller, + createScopedCallerContext, + }), async list_users(params: { limit?: number }, ctx: ToolContext) { const caller = createUserCaller(createScopedCallerContext(ctx)); diff --git a/packages/api/src/router/assistant-tools/config-readmodels.ts b/packages/api/src/router/assistant-tools/config-readmodels.ts new file mode 100644 index 0000000..240fd4b --- /dev/null +++ b/packages/api/src/router/assistant-tools/config-readmodels.ts @@ -0,0 +1,221 @@ +import type { TRPCContext } from "../../trpc.js"; +import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js"; + +type ConfigReadmodelsDeps = { + createManagementLevelCaller: (ctx: TRPCContext) => { + listGroups: () => Promise; + }>>; + }; + createUtilizationCategoryCaller: (ctx: TRPCContext) => { + list: () => Promise>; + getById: (params: { id: string }) => Promise<{ + _count: { projects: number }; + }>; + }; + createCalculationRuleCaller: (ctx: TRPCContext) => { + list: () => Promise>; + }; + createEffortRuleCaller: (ctx: TRPCContext) => { + list: () => Promise; + }>>; + }; + createExperienceMultiplierCaller: (ctx: TRPCContext) => { + list: () => Promise; + }>>; + }; + createScopedCallerContext: (ctx: ToolContext) => TRPCContext; +}; + +export const configReadmodelToolDefinitions: ToolDef[] = [ + { + type: "function", + function: { + name: "list_management_levels", + description: "List management level groups and their levels with target percentages.", + parameters: { type: "object", properties: {} }, + }, + }, + { + type: "function", + function: { + name: "list_utilization_categories", + description: "List utilization categories (cost classification for projects).", + parameters: { type: "object", properties: {} }, + }, + }, + { + type: "function", + function: { + name: "list_calculation_rules", + description: "List calculation rules for cost attribution and chargeability.", + parameters: { type: "object", properties: {} }, + }, + }, + { + type: "function", + function: { + name: "list_effort_rules", + description: "List effort estimation rules with their formulas and conditions.", + parameters: { type: "object", properties: {} }, + }, + }, + { + type: "function", + function: { + name: "list_experience_multipliers", + description: "List experience multipliers that adjust effort estimates based on seniority.", + parameters: { type: "object", properties: {} }, + }, + }, +]; + +export function createConfigReadmodelExecutors( + deps: ConfigReadmodelsDeps, +): Record { + return { + async list_management_levels(_params: Record, ctx: ToolContext) { + const caller = deps.createManagementLevelCaller(deps.createScopedCallerContext(ctx)); + const groups = await caller.listGroups(); + return groups.map((group) => ({ + id: group.id, + name: group.name, + target: group.targetPercentage ? `${group.targetPercentage}%` : null, + levels: group.levels.map((level) => ({ id: level.id, name: level.name })), + })); + }, + + async list_utilization_categories(_params: Record, ctx: ToolContext) { + const caller = deps.createUtilizationCategoryCaller(deps.createScopedCallerContext(ctx)); + const categories = await caller.list(); + const categoriesWithCounts = await Promise.all( + categories.map(async (category) => ({ + category, + projectCount: (await caller.getById({ id: category.id }))._count.projects, + })), + ); + + return categoriesWithCounts.map(({ category, projectCount }) => ({ + id: category.id, + code: category.code, + name: category.name, + description: category.description, + projectCount, + })); + }, + + async list_calculation_rules(_params: Record, ctx: ToolContext) { + const caller = deps.createCalculationRuleCaller(deps.createScopedCallerContext(ctx)); + const rules = await caller.list(); + return rules.map((rule) => ({ + id: rule.id, + name: rule.name, + description: rule.description, + isActive: rule.isActive, + triggerType: rule.triggerType, + orderType: rule.orderType, + costEffect: rule.costEffect, + costReductionPercent: rule.costReductionPercent, + chargeabilityEffect: rule.chargeabilityEffect, + priority: rule.priority, + project: rule.project + ? { + id: rule.project.id, + name: rule.project.name, + shortCode: rule.project.shortCode, + } + : null, + })); + }, + + async list_effort_rules(_params: Record, ctx: ToolContext) { + const caller = deps.createEffortRuleCaller(deps.createScopedCallerContext(ctx)); + const ruleSets = await caller.list(); + return ruleSets.flatMap((ruleSet) => ruleSet.rules.map((rule) => ({ + id: rule.id, + description: rule.description, + scopeType: rule.scopeType, + discipline: rule.discipline, + chapter: rule.chapter, + unitMode: rule.unitMode, + hoursPerUnit: rule.hoursPerUnit, + sortOrder: rule.sortOrder, + ruleSet: { + name: ruleSet.name, + isDefault: ruleSet.isDefault, + }, + }))); + }, + + async list_experience_multipliers(_params: Record, ctx: ToolContext) { + const caller = deps.createExperienceMultiplierCaller(deps.createScopedCallerContext(ctx)); + const multiplierSets = await caller.list(); + return multiplierSets.flatMap((multiplierSet) => multiplierSet.rules.map((rule) => ({ + id: rule.id, + description: rule.description, + chapter: rule.chapter, + location: rule.location, + level: rule.level, + costMultiplier: rule.costMultiplier, + billMultiplier: rule.billMultiplier, + shoringRatio: rule.shoringRatio, + additionalEffortRatio: rule.additionalEffortRatio, + sortOrder: rule.sortOrder, + multiplierSet: { + name: multiplierSet.name, + isDefault: multiplierSet.isDefault, + }, + }))); + }, + }; +}