Files
template-app/modules/knowledge/contract/procedures.ts
T

188 lines
6.1 KiB
TypeScript

// The procedure contracts of the knowledge module: schemas and names, no handler and no port.
//
// The module is a retrieval layer over the app's documents: a collection of text files is indexed
// (in the worker: chunk, embed through the AI gateway, upsert into the vector store), searched
// with cited snippets, and managed through an explicit, idempotent enablement flow. The enablement
// procedure is named enableDocumentAi per the Document AI enablement contract; its permission is the module's
// manage permission, so the admin gate is one line in the manifest.
import { procedure } from "@conusai-kit/core"
import { z } from "zod"
import { COLLECTION_CREATED, DOCUMENT_AI_DISABLED, DOCUMENT_AI_ENABLED } from "./constants.js"
import type { DocumentStatus, ServiceState } from "./statuses.js"
const inProject = { org: z.string(), workspace: z.string(), project: z.string() }
const inOrg = { org: z.string() }
export const collectionName = z
.string()
.min(1)
.max(100)
.regex(/^[a-z][a-z0-9-]*$/, "lower-case letters, digits and dashes")
export const collectionViewSchema = z.object({
name: collectionName,
documentCount: z.number().int(),
indexedCount: z.number().int(),
createdBy: z.string(),
})
export type CollectionView = z.output<typeof collectionViewSchema>
export const documentViewSchema = z.object({
documentId: z.string(),
collectionName: collectionName,
fileId: z.string(),
filename: z.string(),
status: z.enum(["queued", "indexing", "indexed", "failed", "cancelled"]),
error: z.string().nullable(),
indexedAt: z.string().nullable(),
requestedBy: z.string(),
})
export type DocumentView = z.output<typeof documentViewSchema>
export const searchResultSchema = z.object({
fileId: z.string(),
filename: z.string(),
collectionName: collectionName,
snippet: z.string(),
/** 0..1; higher is closer. */
score: z.number(),
/** 1-based chunk position in the document: where a citation points. */
chunk: z.number().int().min(1),
})
export type SearchResult = z.output<typeof searchResultSchema>
export const collectionList = procedure("knowledge.collection.list")
.kind("read")
.scope("project")
.permission("knowledge.knowledge.search")
.input(z.object({ ...inProject }))
.output(z.object({ collections: z.array(collectionViewSchema) }))
.contract()
export const collectionCreate = procedure("knowledge.collection.create")
.kind("write")
.scope("project")
.permission("knowledge.collection.manage")
.input(z.object({ ...inProject, name: collectionName }))
.output(collectionViewSchema)
.audit(COLLECTION_CREATED)
.contract()
export const documentIndex = procedure("knowledge.document.index")
.kind("write")
.scope("project")
.permission("knowledge.knowledge.manage")
.input(z.object({ ...inProject, collectionName: collectionName, fileId: z.string() }))
.output(documentViewSchema)
.contract()
export const documentList = procedure("knowledge.document.list")
.kind("read")
.scope("project")
.permission("knowledge.knowledge.search")
.input(z.object({ ...inProject, collectionName: collectionName }))
.output(z.object({ documents: z.array(documentViewSchema) }))
.contract()
export const documentRemove = procedure("knowledge.document.remove")
.kind("write")
.scope("project")
.permission("knowledge.knowledge.manage")
.input(z.object({ ...inProject, collectionName: collectionName, fileId: z.string() }))
.output(documentViewSchema)
.contract()
/**
* The retrieval call. It embeds the query through the AI gateway before it searches, so it is an
* effect: a read procedure cannot leave the process. Exposed as an MCP tool on purpose (`.tool()`).
*/
export const knowledgeSearch = procedure("knowledge.search")
.kind("effect")
.scope("project")
.permission("knowledge.knowledge.search")
.tool()
.input(
z.object({
...inProject,
collectionName: collectionName,
query: z.string().min(1).max(500),
limit: z.number().int().min(1).max(25).optional(),
}),
)
.output(z.object({ results: z.array(searchResultSchema) }))
.contract()
export const knowledgeStatus = procedure("knowledge.status")
.kind("read")
.scope("project")
.permission("knowledge.knowledge.search")
.input(z.object({ ...inProject }))
.output(
z.object({
/** The module flag's answer for this project. */
enabled: z.boolean(),
/** The service readiness, kept separate from the indexing indicator below. */
service: z.object({
state: z.enum(["registered", "indexing", "ready", "degraded", "disabled", "unavailable"]),
reasons: z.array(z.string()),
}),
documents: z.object({
queued: z.number().int(),
indexing: z.number().int(),
indexed: z.number().int(),
failed: z.number().int(),
cancelled: z.number().int(),
}),
}),
)
.contract()
export const settingsGet = procedure("knowledge.settings.get")
.kind("read")
.scope("org")
.permission("knowledge.settings.manage")
.input(z.object({ ...inOrg }))
.output(
z.object({
enabled: z.boolean(),
backend: z.enum(["vectors", "platform"]).nullable(),
source: z.enum(["operator", "managed"]).nullable(),
url: z.string().nullable(),
}),
)
.contract()
export const settingsEnableDocumentAi = procedure("knowledge.settings.enableDocumentAi")
.kind("write")
.scope("org")
.permission("knowledge.settings.manage")
.input(z.object({ ...inOrg }))
.output(z.object({ enabled: z.boolean(), backend: z.enum(["vectors", "platform"]) }))
.audit(DOCUMENT_AI_ENABLED)
.contract()
export const settingsDisableDocumentAi = procedure("knowledge.settings.disableDocumentAi")
.kind("write")
.scope("org")
.permission("knowledge.settings.manage")
.input(z.object({ ...inOrg }))
.output(z.object({ enabled: z.boolean() }))
.audit(DOCUMENT_AI_DISABLED)
.contract()
export const KNOWLEDGE_PROCEDURES = [
collectionList,
collectionCreate,
documentIndex,
documentList,
documentRemove,
knowledgeSearch,
knowledgeStatus,
settingsGet,
settingsEnableDocumentAi,
settingsDisableDocumentAi,
]
export type { DocumentStatus, ServiceState }