// The ports the knowledge module's use cases are written against, and the tokens the composition // roots bind. Core imports no SDK and no adapter (spec §2): the search service and the enablement // platform are shapes, and adapters/ is the only place an SDK exists. import { type Container, type TenantScope, token } from "@conusai-kit/core" import type { DataSource } from "@conusai-kit/data" import type { FilesDeps } from "@conusai-kit/files" import type { EnablementReason, KnowledgeRefusal, ServiceState } from "../contract/index.js" import { config, KNOWLEDGE_FLAG_DEFAULTS } from "../contract/index.js" /** A project scope: collections and documents live at this level. */ export type KnowledgeScope = TenantScope & { workspaceId: string; projectId: string } /** An org scope: the enablement configuration is the organization's own. */ export type KnowledgeOrgScope = TenantScope & { orgId: string } export interface KnowledgeDeps { readonly data: DataSource readonly files: FilesDeps readonly flags: KnowledgeFlags readonly beforeIndex?: (subjectUserId: string) => Promise /** * The search service. Present for the effect procedure (search); a read or write procedure's deps * never carry it, because resolving an effect port from either is a kind violation — the write * only records the row and publishes the event, and the status read asks `KNOWLEDGE_READINESS`. */ readonly search?: KnowledgeSearchService | undefined /** The document reader and the passage index, for the automatic PDF intake (core-11); bound in the worker. */ readonly reader?: KnowledgeReader | undefined readonly passages?: KnowledgePassageIndex | undefined /** The organization's eligibility and indexing quota, which the PDF intake checks before each read. */ readonly enablement?: DocumentAiPlatform | undefined } /** One cited search answer. */ export interface KnowledgeHit { fileId: string filename: string collectionName: string /** 0..1; higher is closer. */ score: number chunk: number snippet: string page?: number blockId?: string chunkId?: string } /** * The search service: index a document, search a collection with citations, remove a document, and * report readiness. Its one adapter is the platform's managed documents-index service (ADR-061), which * owns chunking, embeddings and the vector store. Every call leaves the process, so this port is an * effect. `refusal` is why the service will not serve this deployment, when that is the answer. */ export interface KnowledgeSearchService { readiness(scope: KnowledgeScope): Promise<{ state: ServiceState; reasons: string[]; refusal?: KnowledgeRefusal }> index( scope: KnowledgeScope, input: { collectionName: string; fileId: string; filename: string; bytes: Uint8Array; signal?: AbortSignal }, ): Promise search( scope: KnowledgeScope, input: { collectionName: string; query: string; limit: number; actorId: string }, ): Promise /** `storageKey` names the object, resolved when the removal was asked for; only an older record lacks it, and then the file is looked up. */ remove(scope: KnowledgeScope, input: { collectionName: string; fileId: string; storageKey?: string }): Promise } /** * One passage of a read PDF, as the automatic intake indexes it: its page (1-based), its place on that * page (0-based; with the page, its stable name across reads of the same document), the document's own * block ids it holds lines of, and its text. */ export interface KnowledgePassage { readonly page: number readonly index: number readonly blockIds: readonly string[] readonly text: string } /** * The platform's document reader, as the automatic intake uses it: a PDF in, its passages out. The * same `idempotencyKey` reads the same document once. A document it can never make passages of throws * with `permanent: "not_indexable"`; anything else is transient, and the queue retries the job. */ export interface KnowledgeReader { read( bytes: Uint8Array, options: { filename: string; idempotencyKey: string; signal?: AbortSignal }, ): Promise } /** * The knowledge service's inline index, one item per passage, each under the file's reference plus its * page and place. The reference is asked for first, so the caller can record what it is about to index * before any of it is sent, and `remove` takes it back because the file may be gone by then: the service * removes a reference only exactly, so removal names every passage that was indexed. */ export interface KnowledgePassageIndex { reference(scope: KnowledgeScope, fileId: string): Promise index( scope: KnowledgeScope, input: { collectionName: string documentRef: string fileId: string passages: readonly KnowledgePassage[] signal?: AbortSignal }, ): Promise remove( scope: KnowledgeScope, input: { documentRef: string; passages: ReadonlyArray> }, ): Promise } /** * What the status read asks of the deployment's search backend. It is a port of its own because a read * handler may not resolve `KNOWLEDGE_SEARCH`, and must not build it: a factory runs with the resolving * handler's restrictions, so a search service built for a read would hold a read-only data source. */ export type KnowledgeReadiness = Pick /** * The enablement platform: the checks the enablement flow needs, over the platform's own APIs. * `checkEligibility` is where the plan capability and the rollout flags surface as typed reasons; * `indexingQuota` answers today's remaining indexing volume. */ export interface DocumentAiPlatform { checkEligibility( scope: KnowledgeOrgScope, ): Promise<{ ok: true } | { ok: false; reason: EnablementReason; detail?: string }> indexingQuota(scope: KnowledgeOrgScope): Promise } /** What a flag service answers for the project a call runs in. */ export interface KnowledgeFlags { enabled(flag: keyof typeof KNOWLEDGE_FLAG_DEFAULTS, scope: KnowledgeScope): boolean | Promise } /** * Bound by an app that evaluates flags (the platform's flag service). Unbound: knowledge is on for an * organization once an administrator has enabled document AI, and off until then (`enablementFlags`). */ export const KNOWLEDGE_FLAGS = token("knowledge.flags") /** The organization's own switch: the row `knowledge.settings.enableDocumentAi` writes. */ export const enablementFlags = (data: DataSource): KnowledgeFlags => ({ enabled: async (flag, scope) => { const row = (await data.forScope(scope).of(config).list({ limit: 1 })).items[0] return row?.enabled ?? KNOWLEDGE_FLAG_DEFAULTS[flag] }, }) export const flagsOf = (container: Container, data: DataSource): KnowledgeFlags => container.has(KNOWLEDGE_FLAGS) ? container.resolve(KNOWLEDGE_FLAGS) : enablementFlags(data) /** Bound by the worker and the web shell with the module's search backend adapter. */ export const KNOWLEDGE_SEARCH = token("knowledge.search", { access: "effect" }) /** Bound by the worker with the platform's document reader: the automatic PDF intake reads through it. */ export const KNOWLEDGE_READER = token("knowledge.reader", { access: "effect" }) /** Bound by the worker with the knowledge service's inline index: the PDF intake's passages, and their removal. */ export const KNOWLEDGE_PASSAGES = token("knowledge.passages", { access: "effect" }) /** Bound by the web shell with the readiness of the service `KNOWLEDGE_SEARCH` serves. */ export const KNOWLEDGE_READINESS = token("knowledge.readiness") /** * Bound by the web shell (the enablement flow runs in a request) and by the worker (the PDF intake checks * eligibility and quota again before each read, core-11). */ export const KNOWLEDGE_ENABLEMENT = token("knowledge.enablement", { access: "effect" }) /** The per-run scope, under the module's own name (decision AT-F6). */ export const KNOWLEDGE_SCOPE = token("knowledge.scope") export const knowledgeEnabled = async (deps: Pick, scope: KnowledgeScope): Promise => deps.flags.enabled("knowledge.enabled", scope)