// 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: the platform's knowledge service reads, chunks and embeds it), 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 { AUTO_INDEX_CHANGED, COLLECTION_CREATED, DOCUMENT_AI_DISABLED, DOCUMENT_AI_ENABLED } from "./constants.js" import { type DocumentStatus, KNOWLEDGE_BACKENDS, KNOWLEDGE_REFUSALS, type 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 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 export const searchResultSchema = z.object({ fileId: z.string(), filename: z.string(), collectionName: collectionName, snippet: z.string(), page: z.number().int().min(1).optional(), blockId: z.string().optional(), chunkId: z.string().optional(), /** 0..1; higher is closer. */ score: z.number(), /** Result ordinal only. Page/block/chunkId carry actual returned source locators when available. */ chunk: z.number().int().min(1), }) export type SearchResult = z.output 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() /** * The live collection's batch-get (contract C-05): the documents with these ids, in whichever collection of * the project, under the list's permission. An id of another tenant, of another project or of nothing is * left out of the answer. */ export const documentGetMany = procedure("knowledge.document.getMany") .kind("read") .scope("project") .permission("knowledge.knowledge.search") .input(z.object({ ...inProject, ids: z.array(z.string().min(1).max(64)).min(1).max(100) })) .output(z.object({ items: 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()), /** Why the service will not serve this deployment, when that is the answer: the page's own state. */ refusal: z.enum(KNOWLEDGE_REFUSALS).optional(), }), 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(KNOWLEDGE_BACKENDS).nullable(), }), ) .contract() /** Its checks and the managed link call the platform's APIs, so it is an effect, like the search. */ export const settingsEnableDocumentAi = procedure("knowledge.settings.enableDocumentAi") .kind("effect") .scope("org") .permission("knowledge.settings.manage") .input(z.object({ ...inOrg })) .output(z.object({ enabled: z.boolean(), backend: z.enum(KNOWLEDGE_BACKENDS) })) .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() /** Whether this project indexes its uploaded PDFs automatically, and into which collection. */ export const autoIndexGet = procedure("knowledge.autoindex.get") .kind("read") .scope("project") .permission("knowledge.knowledge.search") .input(z.object({ ...inProject })) .output(z.object({ collectionName: collectionName.nullable() })) .contract() /** * Turns automatic PDF indexing on (into an existing collection) or off (`null`). The administrator who * turns it on is the one every indexing job re-checks: their permission is the job's authority. */ export const autoIndexSet = procedure("knowledge.autoindex.set") .kind("write") .scope("project") .permission("knowledge.knowledge.manage") .input(z.object({ ...inProject, collectionName: collectionName.nullable() })) .output(z.object({ collectionName: collectionName.nullable() })) .audit(AUTO_INDEX_CHANGED) .contract() export const KNOWLEDGE_PROCEDURES = [ collectionList, collectionCreate, documentIndex, documentList, documentGetMany, documentRemove, knowledgeSearch, knowledgeStatus, settingsGet, settingsEnableDocumentAi, settingsDisableDocumentAi, autoIndexGet, autoIndexSet, ] export type { DocumentStatus, ServiceState }