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)" },
|
budgetCents: { type: "integer", description: "New budget in cents (e.g. 10000000 = 100,000 EUR)" },
|
||||||
winProbability: { type: "integer", description: "Win probability 0-100" },
|
winProbability: { type: "integer", description: "Win probability 0-100" },
|
||||||
status: { type: "string", description: "New status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED" },
|
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"],
|
required: ["id"],
|
||||||
},
|
},
|
||||||
@@ -396,7 +397,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
|||||||
endDate: { type: "string", description: "End date (YYYY-MM-DD)" },
|
endDate: { type: "string", description: "End date (YYYY-MM-DD)" },
|
||||||
winProbability: { type: "integer", description: "Win probability 0-100. Default: 100" },
|
winProbability: { type: "integer", description: "Win probability 0-100. Default: 100" },
|
||||||
status: { type: "string", description: "Initial status: DRAFT, ACTIVE, ON_HOLD. Default: DRAFT" },
|
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')" },
|
color: { type: "string", description: "Hex color (e.g. '#3b82f6')" },
|
||||||
blueprintName: { type: "string", description: "Blueprint name to look up and attach (partial match)" },
|
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)" },
|
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 ─────────────────────────────────────────────────────────
|
// ─── Tool Executors ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const executors = {
|
const executors = {
|
||||||
@@ -1389,6 +1418,7 @@ const executors = {
|
|||||||
async update_project(params: {
|
async update_project(params: {
|
||||||
id: string; name?: string; budgetCents?: number;
|
id: string; name?: string; budgetCents?: number;
|
||||||
winProbability?: number; status?: string;
|
winProbability?: number; status?: string;
|
||||||
|
responsiblePerson?: string;
|
||||||
}, ctx: ToolContext) {
|
}, ctx: ToolContext) {
|
||||||
assertPermission(ctx, "manageProjects" as PermissionKey);
|
assertPermission(ctx, "manageProjects" as PermissionKey);
|
||||||
const data: Record<string, unknown> = {};
|
const data: Record<string, unknown> = {};
|
||||||
@@ -1397,6 +1427,13 @@ const executors = {
|
|||||||
if (params.winProbability !== undefined) data.winProbability = params.winProbability;
|
if (params.winProbability !== undefined) data.winProbability = params.winProbability;
|
||||||
if (params.status !== undefined) data.status = params.status;
|
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" };
|
if (Object.keys(data).length === 0) return { error: "No fields to update" };
|
||||||
|
|
||||||
const project = await ctx.db.project.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 (isNaN(endDate.getTime())) return { error: `Invalid endDate: ${params.endDate}` };
|
||||||
if (endDate < startDate) return { error: "endDate must be after startDate" };
|
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
|
// Optional: look up blueprint by name
|
||||||
let blueprintId: string | undefined;
|
let blueprintId: string | undefined;
|
||||||
if (params.blueprintName) {
|
if (params.blueprintName) {
|
||||||
@@ -1486,7 +1531,7 @@ const executors = {
|
|||||||
endDate,
|
endDate,
|
||||||
winProbability: params.winProbability ?? 100,
|
winProbability: params.winProbability ?? 100,
|
||||||
status,
|
status,
|
||||||
...(params.responsiblePerson ? { responsiblePerson: params.responsiblePerson } : {}),
|
...(resolvedResponsible ? { responsiblePerson: resolvedResponsible } : {}),
|
||||||
...(params.color ? { color: params.color } : {}),
|
...(params.color ? { color: params.color } : {}),
|
||||||
...(blueprintId ? { blueprintId } : {}),
|
...(blueprintId ? { blueprintId } : {}),
|
||||||
...(clientId ? { clientId } : {}),
|
...(clientId ? { clientId } : {}),
|
||||||
|
|||||||
Reference in New Issue
Block a user