fix: validate responsiblePerson against existing resources in bot tools
The create_project and update_project AI assistant tools now resolve the responsiblePerson field against active resources (case-insensitive). This ensures the name matches an existing resource so dashboard widgets like "My Projects" can correctly link projects to people. Closes #15 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -374,6 +374,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
budgetCents: { type: "integer", description: "New budget in cents (e.g. 10000000 = 100,000 EUR)" },
|
||||
winProbability: { type: "integer", description: "Win probability 0-100" },
|
||||
status: { type: "string", description: "New status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED" },
|
||||
responsiblePerson: { type: "string", description: "Name of the responsible person. Must match an existing resource's display name (case-insensitive search)." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
@@ -396,7 +397,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
endDate: { type: "string", description: "End date (YYYY-MM-DD)" },
|
||||
winProbability: { type: "integer", description: "Win probability 0-100. Default: 100" },
|
||||
status: { type: "string", description: "Initial status: DRAFT, ACTIVE, ON_HOLD. Default: DRAFT" },
|
||||
responsiblePerson: { type: "string", description: "Name of the responsible person" },
|
||||
responsiblePerson: { type: "string", description: "Name of the responsible person. Must match an existing resource's display name (case-insensitive search)." },
|
||||
color: { type: "string", description: "Hex color (e.g. '#3b82f6')" },
|
||||
blueprintName: { type: "string", description: "Blueprint name to look up and attach (partial match)" },
|
||||
clientName: { type: "string", description: "Client name to look up and attach (partial match)" },
|
||||
@@ -407,6 +408,34 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Resolve a responsible person name against existing resources. Returns the exact displayName or an error object. */
|
||||
async function resolveResponsiblePerson(
|
||||
name: string,
|
||||
db: ToolContext["db"],
|
||||
): Promise<{ displayName: string } | { error: string }> {
|
||||
// Exact match first (case-insensitive)
|
||||
const exact = await db.resource.findFirst({
|
||||
where: { displayName: { equals: name, mode: "insensitive" }, isActive: true },
|
||||
select: { displayName: true },
|
||||
});
|
||||
if (exact) return { displayName: exact.displayName };
|
||||
|
||||
// Fuzzy: contains search
|
||||
const candidates = await db.resource.findMany({
|
||||
where: { displayName: { contains: name, mode: "insensitive" }, isActive: true },
|
||||
select: { displayName: true, eid: true },
|
||||
take: 5,
|
||||
});
|
||||
if (candidates.length === 1) return { displayName: candidates[0]!.displayName };
|
||||
if (candidates.length > 1) {
|
||||
const list = candidates.map((c) => `${c.displayName} (${c.eid})`).join(", ");
|
||||
return { error: `Multiple resources match "${name}": ${list}. Please specify the exact name.` };
|
||||
}
|
||||
return { error: `No active resource found matching "${name}". The responsible person must be an existing resource.` };
|
||||
}
|
||||
|
||||
// ─── Tool Executors ─────────────────────────────────────────────────────────
|
||||
|
||||
const executors = {
|
||||
@@ -1389,6 +1418,7 @@ const executors = {
|
||||
async update_project(params: {
|
||||
id: string; name?: string; budgetCents?: number;
|
||||
winProbability?: number; status?: string;
|
||||
responsiblePerson?: string;
|
||||
}, ctx: ToolContext) {
|
||||
assertPermission(ctx, "manageProjects" as PermissionKey);
|
||||
const data: Record<string, unknown> = {};
|
||||
@@ -1397,6 +1427,13 @@ const executors = {
|
||||
if (params.winProbability !== undefined) data.winProbability = params.winProbability;
|
||||
if (params.status !== undefined) data.status = params.status;
|
||||
|
||||
// Validate responsible person against existing resources
|
||||
if (params.responsiblePerson !== undefined) {
|
||||
const result = await resolveResponsiblePerson(params.responsiblePerson, ctx.db);
|
||||
if ("error" in result) return { error: result.error };
|
||||
data.responsiblePerson = result.displayName;
|
||||
}
|
||||
|
||||
if (Object.keys(data).length === 0) return { error: "No fields to update" };
|
||||
|
||||
const project = await ctx.db.project.update({
|
||||
@@ -1453,6 +1490,14 @@ const executors = {
|
||||
if (isNaN(endDate.getTime())) return { error: `Invalid endDate: ${params.endDate}` };
|
||||
if (endDate < startDate) return { error: "endDate must be after startDate" };
|
||||
|
||||
// Validate responsible person against existing resources
|
||||
let resolvedResponsible: string | undefined;
|
||||
if (params.responsiblePerson) {
|
||||
const result = await resolveResponsiblePerson(params.responsiblePerson, ctx.db);
|
||||
if ("error" in result) return { error: result.error };
|
||||
resolvedResponsible = result.displayName;
|
||||
}
|
||||
|
||||
// Optional: look up blueprint by name
|
||||
let blueprintId: string | undefined;
|
||||
if (params.blueprintName) {
|
||||
@@ -1486,7 +1531,7 @@ const executors = {
|
||||
endDate,
|
||||
winProbability: params.winProbability ?? 100,
|
||||
status,
|
||||
...(params.responsiblePerson ? { responsiblePerson: params.responsiblePerson } : {}),
|
||||
...(resolvedResponsible ? { responsiblePerson: resolvedResponsible } : {}),
|
||||
...(params.color ? { color: params.color } : {}),
|
||||
...(blueprintId ? { blueprintId } : {}),
|
||||
...(clientId ? { clientId } : {}),
|
||||
|
||||
Reference in New Issue
Block a user