// 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, ServiceState } from "../contract/index.js" import { 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 /** * The search service. Present for the effect procedure (search) and the status read; a write * procedure's deps never carry it, because resolving an effect port from a write is a kind * violation — the write only records the row and publishes the event. */ readonly search?: KnowledgeSearchService | undefined } /** What a chunk knows about where it came from: the citation a search result carries. */ export interface KnowledgeChunk { /** 1-based position of the chunk in the document. */ position: number text: string } /** One cited search answer. */ export interface KnowledgeHit { fileId: string filename: string collectionName: string /** 0..1; higher is closer. */ score: number chunk: number snippet: string } /** * The search service: index a document's chunks, search a collection with citations, remove a * document, and report readiness. Two adapters ship with the module: the local vector store * (embeddings through the AI gateway, chunks in the bound vector adapter) and the platform's * managed documents-index service. Embedding leaves the process, so this port is an effect. */ export interface KnowledgeSearchService { readiness(scope: KnowledgeScope): Promise<{ state: ServiceState; reasons: string[] }> index( scope: KnowledgeScope, input: { collectionName: string; fileId: string; filename: string; bytes: Uint8Array }, ): Promise search( scope: KnowledgeScope, input: { collectionName: string; query: string; limit: number; actorId: string }, ): Promise remove(scope: KnowledgeScope, input: { collectionName: string; fileId: string }): Promise } /** * The enablement platform: the checks and the provisioning 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; * `linkManagedQdrant` provisions (or returns the existing) managed Qdrant backing service; * `probeQdrant` validates an operator-configured endpoint before its configuration is persisted. */ export interface DocumentAiPlatform { checkEligibility( scope: KnowledgeOrgScope, ): Promise<{ ok: true } | { ok: false; reason: EnablementReason; detail?: string }> indexingQuota(scope: KnowledgeOrgScope): Promise linkManagedQdrant(scope: KnowledgeOrgScope): Promise<{ serviceId: string; url: string | null }> probeQdrant(url: string, apiKey: string): Promise<{ ok: true } | { ok: false; detail: string }> /** The operator-configured endpoint+key the deployment carries, when it carries one. */ operatorQdrant(): { url: string; apiKey: string } | null } /** 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: the manifest default. */ export const KNOWLEDGE_FLAGS = token("knowledge.flags") export const defaultFlags: KnowledgeFlags = { enabled: (flag) => KNOWLEDGE_FLAG_DEFAULTS[flag] } export const flagsOf = (container: Container): KnowledgeFlags => container.has(KNOWLEDGE_FLAGS) ? container.resolve(KNOWLEDGE_FLAGS) : defaultFlags /** Bound by the worker and the web shell with the module's search backend adapter. */ export const KNOWLEDGE_SEARCH = token("knowledge.search", { access: "effect" }) /** * The embeddings the local vector backend chunks and queries with: the platform's embeddings * endpoint through the AI gateway. Declared here (not reached through another module's token) so * the module's boundary stays its own; the composition root binds it to the gateway client. */ export interface KnowledgeEmbeddings { embed(texts: readonly string[], options?: { inputType?: "passage" | "query" }): Promise } export const KNOWLEDGE_EMBEDDINGS = token("knowledge.embeddings", { access: "effect" }) /** Bound by the web shell (the enablement flow runs in a request). */ 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: KnowledgeDeps, scope: KnowledgeScope): Promise => deps.flags.enabled("knowledge.enabled", scope)