Files
template-app/modules/activities/contract/index.ts
T
2026-11-14 09:56:45 +00:00

105 lines
3.9 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Project-scoped task and comment contracts. Authority comes from resolved scope.
import { procedure } from "@conusai-kit/core"
import { z } from "zod"
const inProject = { org: z.string(), workspace: z.string(), project: z.string() }
const id = z.string().uuid()
const resourceSuffix = "[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
export const activityId = z.string().regex(new RegExp(`^act_${resourceSuffix}$`))
const commentId = z.string().regex(new RegExp(`^cmt_${resourceSuffix}$`))
// Cursors belong to the service and are forwarded unchanged; they are not resource IDs.
const cursor = z.string().min(1).max(4096)
const revision = z.string().regex(/^[1-9][0-9]{0,18}$/)
export const taskView = z.object({
taskId: activityId,
subject: z.string(),
body: z.string(),
revision,
status: z.enum(["open", "done", "cancelled"]),
createdAt: z.string(),
})
export type TaskView = z.output<typeof taskView>
export const commentView = z.object({
commentId,
body: z.string(),
author: z.string().nullable(),
erased: z.boolean(),
})
export type CommentView = z.output<typeof commentView>
export const commentReceipt = z.object({ commentId, revision })
export type CommentReceipt = z.output<typeof commentReceipt>
export const receipt = z.object({ taskId: activityId, revision })
export type TaskReceipt = z.output<typeof receipt>
const write = { ...inProject, taskId: activityId, expectedVersion: revision, requestId: id }
export const taskList = procedure("activities.task.list")
.kind("read")
.scope("project")
.permission("activities.task.read")
.input(z.object({ ...inProject, status: z.enum(["open", "done", "cancelled"]).optional(), after: cursor.optional() }))
.output(z.object({ rows: z.array(taskView), after: cursor.optional() }))
.contract()
export const taskGet = procedure("activities.task.get")
.kind("read")
.scope("project")
.permission("activities.task.read")
.input(z.object({ ...inProject, taskId: activityId }))
.output(taskView)
.contract()
export const taskCreate = procedure("activities.task.create")
.kind("effect")
.scope("project")
.permission("activities.task.write")
.input(
z.object({
...inProject,
subject: z.string().trim().min(1).max(200),
body: z.string().max(10000).optional(),
requestId: id,
}),
)
.output(receipt)
.contract()
export const taskUpdate = procedure("activities.task.update")
.kind("effect")
.scope("project")
.permission("activities.task.write")
.input(z.object({ ...write, subject: z.string().trim().min(1).max(200), body: z.string().max(10000) }))
.output(receipt)
.contract()
export const taskComplete = procedure("activities.task.complete")
.kind("effect")
.scope("project")
.permission("activities.task.write")
.input(z.object(write))
.output(receipt)
.contract()
export const taskReopen = procedure("activities.task.reopen")
.kind("effect")
.scope("project")
.permission("activities.task.write")
.input(z.object(write))
.output(receipt)
.contract()
export const commentList = procedure("activities.comment.list")
.kind("read")
.scope("project")
.permission("activities.task.read")
.input(z.object({ ...inProject, taskId: activityId, after: cursor.optional() }))
.output(z.object({ rows: z.array(commentView), after: cursor.optional() }))
.contract()
export const commentAdd = procedure("activities.comment.add")
.kind("effect")
.scope("project")
.permission("activities.comment.write")
.input(z.object({ ...write, body: z.string().trim().min(1).max(10000) }))
.output(commentReceipt)
.contract()
export const timeline = procedure("activities.timeline.list")
.kind("read")
.scope("project")
.permission("activities.task.read")
.input(z.object({ ...inProject, after: cursor.optional() }))
.output(z.object({ rows: z.array(taskView), after: cursor.optional() }))
.contract()