refactor(api): extract assistant comments slice
This commit is contained in:
@@ -40,12 +40,13 @@
|
|||||||
- 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
|
- 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 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
|
||||||
|
|
||||||
## 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 collaboration or analytics block such as comments/audit helpers or 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 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.
|
||||||
|
|
||||||
## Remaining Major Themes
|
## Remaining Major Themes
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import {
|
|||||||
CreateAssignmentSchema,
|
CreateAssignmentSchema,
|
||||||
AllocationStatus,
|
AllocationStatus,
|
||||||
EstimateStatus,
|
EstimateStatus,
|
||||||
type CommentEntityType,
|
|
||||||
COMMENT_ENTITY_TYPE_VALUES,
|
|
||||||
PermissionKey,
|
PermissionKey,
|
||||||
SystemRole,
|
SystemRole,
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
@@ -129,6 +127,11 @@ import {
|
|||||||
createDashboardInsightsReportsExecutors,
|
createDashboardInsightsReportsExecutors,
|
||||||
dashboardInsightsReportsToolDefinitions,
|
dashboardInsightsReportsToolDefinitions,
|
||||||
} from "./assistant-tools/dashboard-insights-reports.js";
|
} from "./assistant-tools/dashboard-insights-reports.js";
|
||||||
|
import {
|
||||||
|
commentMutationToolDefinitions,
|
||||||
|
commentReadToolDefinitions,
|
||||||
|
createCommentExecutors,
|
||||||
|
} from "./assistant-tools/comments.js";
|
||||||
import {
|
import {
|
||||||
withToolAccess,
|
withToolAccess,
|
||||||
type ToolAccessRequirements,
|
type ToolAccessRequirements,
|
||||||
@@ -136,7 +139,6 @@ import {
|
|||||||
type ToolDef,
|
type ToolDef,
|
||||||
type ToolExecutor,
|
type ToolExecutor,
|
||||||
} from "./assistant-tools/shared.js";
|
} from "./assistant-tools/shared.js";
|
||||||
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../lib/comment-entity-registry.js";
|
|
||||||
|
|
||||||
export type { ToolContext } from "./assistant-tools/shared.js";
|
export type { ToolContext } from "./assistant-tools/shared.js";
|
||||||
|
|
||||||
@@ -2319,22 +2321,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
|
|||||||
|
|
||||||
// ── TASK MANAGEMENT ──
|
// ── TASK MANAGEMENT ──
|
||||||
...notificationTaskToolDefinitions,
|
...notificationTaskToolDefinitions,
|
||||||
|
...commentReadToolDefinitions,
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_comments",
|
|
||||||
description: `List comments (with replies) for a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required.`,
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
|
||||||
entityId: { type: "string", description: "Entity ID" },
|
|
||||||
},
|
|
||||||
required: ["entityType", "entityId"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
@@ -2399,37 +2386,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
...commentMutationToolDefinitions,
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "create_comment",
|
|
||||||
description: `Add a comment to a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required. Supports @mentions. Always confirm with the user first.`,
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
|
||||||
entityId: { type: "string", description: "Entity ID" },
|
|
||||||
body: { type: "string", description: "Comment body text. Use @[Name](userId) for mentions." },
|
|
||||||
},
|
|
||||||
required: ["entityType", "entityId", "body"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "resolve_comment",
|
|
||||||
description: `Mark a comment as resolved (or unresolve it) on a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required, and only the comment author or an admin can change resolution.`,
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
commentId: { type: "string", description: "Comment ID to resolve" },
|
|
||||||
resolved: { type: "boolean", description: "Set to true to resolve, false to unresolve. Default: true" },
|
|
||||||
},
|
|
||||||
required: ["commentId"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
@@ -2861,6 +2818,12 @@ const executors = {
|
|||||||
createReportCaller,
|
createReportCaller,
|
||||||
createScopedCallerContext,
|
createScopedCallerContext,
|
||||||
}),
|
}),
|
||||||
|
...createCommentExecutors({
|
||||||
|
createCommentCaller,
|
||||||
|
createScopedCallerContext,
|
||||||
|
toAssistantCommentCreationError,
|
||||||
|
toAssistantCommentResolveError,
|
||||||
|
}),
|
||||||
|
|
||||||
async search_estimates(params: {
|
async search_estimates(params: {
|
||||||
projectCode?: string; query?: string; status?: string; limit?: number;
|
projectCode?: string; query?: string; status?: string; limit?: number;
|
||||||
@@ -3324,30 +3287,6 @@ const executors = {
|
|||||||
toAssistantNotificationCreationError,
|
toAssistantNotificationCreationError,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
async list_comments(params: { entityType: CommentEntityType; entityId: string }, ctx: ToolContext) {
|
|
||||||
const caller = createCommentCaller(createScopedCallerContext(ctx));
|
|
||||||
const comments = await caller.list({
|
|
||||||
entityType: params.entityType,
|
|
||||||
entityId: params.entityId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return comments.map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
author: c.author.name ?? c.author.email,
|
|
||||||
body: c.body,
|
|
||||||
resolved: c.resolved,
|
|
||||||
createdAt: c.createdAt.toISOString(),
|
|
||||||
replyCount: c.replies.length,
|
|
||||||
replies: c.replies.map((r) => ({
|
|
||||||
id: r.id,
|
|
||||||
author: r.author.name ?? r.author.email,
|
|
||||||
body: r.body,
|
|
||||||
resolved: r.resolved,
|
|
||||||
createdAt: r.createdAt.toISOString(),
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
async lookup_rate(params: {
|
async lookup_rate(params: {
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
chapter?: string;
|
chapter?: string;
|
||||||
@@ -3425,74 +3364,6 @@ const executors = {
|
|||||||
return caller.generateProjectNarrative({ projectId: params.projectId });
|
return caller.generateProjectNarrative({ projectId: params.projectId });
|
||||||
},
|
},
|
||||||
|
|
||||||
async create_comment(params: {
|
|
||||||
entityType: CommentEntityType;
|
|
||||||
entityId: string;
|
|
||||||
body: string;
|
|
||||||
}, ctx: ToolContext) {
|
|
||||||
if (params.body.length === 0) {
|
|
||||||
return { error: "Comment body is required." };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.body.length > 10_000) {
|
|
||||||
return { error: "Comment body must be at most 10000 characters." };
|
|
||||||
}
|
|
||||||
|
|
||||||
const caller = createCommentCaller(createScopedCallerContext(ctx));
|
|
||||||
let comment;
|
|
||||||
try {
|
|
||||||
comment = await caller.create({
|
|
||||||
entityType: params.entityType,
|
|
||||||
entityId: params.entityId,
|
|
||||||
body: params.body,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const mapped = toAssistantCommentCreationError(error);
|
|
||||||
if (mapped) {
|
|
||||||
return mapped;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
__action: "invalidate",
|
|
||||||
scope: ["comment"],
|
|
||||||
id: comment.id,
|
|
||||||
author: comment.author.name ?? comment.author.email,
|
|
||||||
body: comment.body,
|
|
||||||
createdAt: comment.createdAt.toISOString(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async resolve_comment(params: {
|
|
||||||
commentId: string;
|
|
||||||
resolved?: boolean;
|
|
||||||
}, ctx: ToolContext) {
|
|
||||||
const caller = createCommentCaller(createScopedCallerContext(ctx));
|
|
||||||
let updated;
|
|
||||||
try {
|
|
||||||
updated = await caller.resolve({
|
|
||||||
id: params.commentId,
|
|
||||||
resolved: params.resolved !== false,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const mapped = toAssistantCommentResolveError(error);
|
|
||||||
if (mapped) {
|
|
||||||
return mapped;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
__action: "invalidate",
|
|
||||||
scope: ["comment"],
|
|
||||||
id: updated.id,
|
|
||||||
resolved: updated.resolved,
|
|
||||||
author: updated.author.name ?? updated.author.email,
|
|
||||||
body: updated.body.slice(0, 100),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async query_change_history(params: {
|
async query_change_history(params: {
|
||||||
entityType?: string;
|
entityType?: string;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { COMMENT_ENTITY_TYPE_VALUES, type CommentEntityType } from "@capakraken/shared";
|
||||||
|
import type { TRPCContext } from "../../trpc.js";
|
||||||
|
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../../lib/comment-entity-registry.js";
|
||||||
|
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||||
|
|
||||||
|
type AssistantToolErrorResult = { error: string };
|
||||||
|
|
||||||
|
type CommentRecord = {
|
||||||
|
id: string;
|
||||||
|
body: string;
|
||||||
|
resolved: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
author: {
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommentListRecord = CommentRecord & {
|
||||||
|
replies: CommentRecord[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommentsDeps = {
|
||||||
|
createCommentCaller: (ctx: TRPCContext) => {
|
||||||
|
list: (params: {
|
||||||
|
entityType: CommentEntityType;
|
||||||
|
entityId: string;
|
||||||
|
}) => Promise<CommentListRecord[]>;
|
||||||
|
create: (params: {
|
||||||
|
entityType: CommentEntityType;
|
||||||
|
entityId: string;
|
||||||
|
body: string;
|
||||||
|
}) => Promise<CommentRecord>;
|
||||||
|
resolve: (params: {
|
||||||
|
id: string;
|
||||||
|
resolved: boolean;
|
||||||
|
}) => Promise<CommentRecord>;
|
||||||
|
};
|
||||||
|
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||||
|
toAssistantCommentCreationError: (
|
||||||
|
error: unknown,
|
||||||
|
) => AssistantToolErrorResult | null;
|
||||||
|
toAssistantCommentResolveError: (
|
||||||
|
error: unknown,
|
||||||
|
) => AssistantToolErrorResult | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commentReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_comments",
|
||||||
|
description: `List comments (with replies) for a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required.`,
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
||||||
|
entityId: { type: "string", description: "Entity ID" },
|
||||||
|
},
|
||||||
|
required: ["entityType", "entityId"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], {});
|
||||||
|
|
||||||
|
export const commentMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "create_comment",
|
||||||
|
description: `Add a comment to a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required. Supports @mentions. Always confirm with the user first.`,
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
||||||
|
entityId: { type: "string", description: "Entity ID" },
|
||||||
|
body: { type: "string", description: "Comment body text. Use @[Name](userId) for mentions." },
|
||||||
|
},
|
||||||
|
required: ["entityType", "entityId", "body"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "resolve_comment",
|
||||||
|
description: `Mark a comment as resolved (or unresolve it) on a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required, and only the comment author or an admin can change resolution.`,
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
commentId: { type: "string", description: "Comment ID to resolve" },
|
||||||
|
resolved: { type: "boolean", description: "Set to true to resolve, false to unresolve. Default: true" },
|
||||||
|
},
|
||||||
|
required: ["commentId"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], {});
|
||||||
|
|
||||||
|
export function createCommentExecutors(
|
||||||
|
deps: CommentsDeps,
|
||||||
|
): Record<string, ToolExecutor> {
|
||||||
|
return {
|
||||||
|
async list_comments(
|
||||||
|
params: { entityType: CommentEntityType; entityId: string },
|
||||||
|
ctx: ToolContext,
|
||||||
|
) {
|
||||||
|
const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
const comments = await caller.list({
|
||||||
|
entityType: params.entityType,
|
||||||
|
entityId: params.entityId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return comments.map((comment) => ({
|
||||||
|
id: comment.id,
|
||||||
|
author: comment.author.name ?? comment.author.email,
|
||||||
|
body: comment.body,
|
||||||
|
resolved: comment.resolved,
|
||||||
|
createdAt: comment.createdAt.toISOString(),
|
||||||
|
replyCount: comment.replies.length,
|
||||||
|
replies: comment.replies.map((reply) => ({
|
||||||
|
id: reply.id,
|
||||||
|
author: reply.author.name ?? reply.author.email,
|
||||||
|
body: reply.body,
|
||||||
|
resolved: reply.resolved,
|
||||||
|
createdAt: reply.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
async create_comment(
|
||||||
|
params: { entityType: CommentEntityType; entityId: string; body: string },
|
||||||
|
ctx: ToolContext,
|
||||||
|
) {
|
||||||
|
if (params.body.length === 0) {
|
||||||
|
return { error: "Comment body is required." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.body.length > 10_000) {
|
||||||
|
return { error: "Comment body must be at most 10000 characters." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
let comment;
|
||||||
|
try {
|
||||||
|
comment = await caller.create({
|
||||||
|
entityType: params.entityType,
|
||||||
|
entityId: params.entityId,
|
||||||
|
body: params.body,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const mapped = deps.toAssistantCommentCreationError(error);
|
||||||
|
if (mapped) {
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
__action: "invalidate",
|
||||||
|
scope: ["comment"],
|
||||||
|
id: comment.id,
|
||||||
|
author: comment.author.name ?? comment.author.email,
|
||||||
|
body: comment.body,
|
||||||
|
createdAt: comment.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async resolve_comment(
|
||||||
|
params: { commentId: string; resolved?: boolean },
|
||||||
|
ctx: ToolContext,
|
||||||
|
) {
|
||||||
|
const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx));
|
||||||
|
let updated;
|
||||||
|
try {
|
||||||
|
updated = await caller.resolve({
|
||||||
|
id: params.commentId,
|
||||||
|
resolved: params.resolved !== false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const mapped = deps.toAssistantCommentResolveError(error);
|
||||||
|
if (mapped) {
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
__action: "invalidate",
|
||||||
|
scope: ["comment"],
|
||||||
|
id: updated.id,
|
||||||
|
resolved: updated.resolved,
|
||||||
|
author: updated.author.name ?? updated.author.email,
|
||||||
|
body: updated.body.slice(0, 100),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user