/** * GNO SDK client. * * @module src/sdk/client */ import { mkdir } from "node:fs/promises"; import { dirname } from "node:path"; import type { Config } from "../config/types"; import type { DownloadPolicy } from "../llm/policy"; import type { EmbeddingPort, GenerationPort, RerankPort } from "../llm/types"; import type { AskResult, SearchResults } from "../pipeline/types"; import type { StoreResult } from "../store/types"; import type { VectorIndexPort } from "../store/vector"; import type { GnoAskOptions, GnoCaptureOptions, GnoCaptureResult, GnoClient, GnoCreateFolderOptions, GnoCreateFolderResult, GnoCreateNoteOptions, GnoCreateNoteResult, GnoContextInput, GnoContextResult, GnoContextVerificationResult, GnoClientInitOptions, GnoDuplicateNoteOptions, GnoEmbedOptions, GnoEmbedResult, GnoGetOptions, GnoIndexOptions, GnoIndexResult, GnoIndexStatus, GnoListOptions, GnoMoveNoteApplyOptions, GnoMoveNoteOptions, GnoMultiGetOptions, GnoQueryOptions, GnoRecallInput, GnoRecallResult, GnoRefactorNoteResult, GnoRememberInput, GnoRememberResult, GnoRenameNoteApplyOptions, GnoRenameNoteOptions, GnoSearchOptions, GnoUpdateOptions, GnoVectorSearchOptions, KnowledgeChangesResult, KnowledgeDiffResult, KnowledgeImpactInput, KnowledgeImpactResult, ListKnowledgeChangesInput, SectionTargetCreateResult, SectionTargetCreateSelector, SectionTargetResolveResult, SectionTargetV1, } from "./types"; import { decorateUriForIndex, DEFAULT_INDEX_NAME, getIndexDbPath, } from "../app/constants"; import { buildContextCapsule, validateContextCapsuleBuildInput, verifyContextCapsuleRuntime, } from "../app/context-runtime"; import { canonicalizeIndexName, INDEX_NAME_REQUIREMENTS, isValidIndexName, } from "../app/index-name"; import { buildVerifiedAsk } from "../app/verified-ask"; import { buildContentTypeBoostStatus, ConfigSchema, loadConfig, normalizeConfigContentTypes, normalizeContentTypes, } from "../config"; import { buildCaptureReceipt, type CapturePlan, listCaptureDiskRelPaths, planCapture, } from "../core/capture"; import { writeCapturePlanFile } from "../core/capture-write"; import { projectCollectionEgressPolicy } from "../core/collection-egress-policy-projection"; import { CollectionEgressPolicyService } from "../core/collection-egress-policy-service"; import { applyConfigChange } from "../core/config-mutation"; import { getDocumentCapabilities } from "../core/document-capabilities"; import { EgressAuditService } from "../core/egress-audit"; import { authorizeCurrentEgress } from "../core/egress-authorization"; import { atomicWrite, copyFilePath, createFolderPath } from "../core/file-ops"; import { applyCanonicalFileRefactor, assertFileRefactorSyncConverged, buildCanonicalRefactorPlan, buildDurableFileRefactorApplyDeps, buildRefactorWarnings, parseRefactorApplyConfirmation, planCreateFolder, planDuplicateRefactor, resolveMoveTarget, resolveRenameTarget, type FileRefactorApplyResult, type FileRefactorPreviewPlan, } from "../core/file-refactors"; import { resolveEffectiveIndex } from "../core/indexed-reference"; import { analyzeKnowledgeImpact, getKnowledgeDiff, listKnowledgeChanges, type KnowledgeDeltaServiceResult, } from "../core/knowledge-delta"; import { MemoryError, type MemoryErrorCode, MemoryService, } from "../core/memory"; import { resolveNoteCreatePlan } from "../core/note-creation"; import { resolveNotePreset } from "../core/note-presets"; import { ProjectAffinityInputError, resolveRemoteProjectAffinity, } from "../core/project-affinity-surface"; import { RetrievalTraceManagementService } from "../core/retrieval-trace-management"; import { finishRetrievalTraceAfterError, retrievalTraceFilters, startRetrievalTraceRequest, } from "../core/retrieval-trace-request"; import { attachRetrievalTraceMetadata, getRetrievalTraceMetadata, RETRIEVAL_TRACE_METADATA, type RetrievalTraceSession, } from "../core/retrieval-trace-session"; import { CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS, createSectionTarget as createSectionTargetCore, extractSections, isTransportBoundedCanonicalUri, parseSectionTargetCreateSelector, parseSectionTargetV1, projectSectionTargetCreateResult, projectSectionTargetResolveResult, resolveSectionTarget as resolveSectionTargetCore, } from "../core/sections"; import { normalizeStructuredQueryInput } from "../core/structured-query"; import { parseAndValidateTagFilter } from "../core/tags"; import { normalizeMetadataPredicate, type MetadataPredicate, } from "../core/typed-metadata"; import { writeLeasePath } from "../core/write-lease"; import { defaultSyncService, type SyncResult, withContentTypeRules, } from "../ingestion"; import { updateFrontmatterTags } from "../ingestion/frontmatter"; import { withInferenceScope, finishInferenceCleanup, } from "../llm/inference-scope"; import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter"; import { resolveDownloadPolicy } from "../llm/policy"; import { resolveModelUri } from "../llm/registry"; import { answerTraceTerminalStatus, generateGroundedAnswer, processAnswerResultWithTrace, } from "../pipeline/answer"; import { formatQueryForEmbedding } from "../pipeline/contextual"; import { searchHybrid } from "../pipeline/hybrid"; import { RequestHydration } from "../pipeline/hydration"; import { searchBm25 } from "../pipeline/search"; import { searchVectorWithEmbedding } from "../pipeline/vsearch"; import { SqliteAdapter } from "../store/sqlite/adapter"; import { openScopedIndexStore } from "../store/sqlite/scoped-index"; import { createVectorIndexPort } from "../store/vector"; import { getDocumentByRef, listDocuments, multiGetDocuments, } from "./documents"; import { runEmbed } from "./embed"; import { type GnoSdkErrorCode, sdkError } from "./errors"; interface OpenedClientState { config: Config; configPath: string | null; configSource: "file" | "inline"; dbPath: string; store: SqliteAdapter; llm: LlmAdapter; downloadPolicy: DownloadPolicy; indexName: string; } interface RuntimePorts { embedPort: EmbeddingPort | null; expandPort: GenerationPort | null; answerPort: GenerationPort | null; rerankPort: RerankPort | null; vectorIndex: VectorIndexPort | null; } const normalizeSdkMetadataFilter = (value: unknown): MetadataPredicate => { try { return normalizeMetadataPredicate(value); } catch (error) { throw sdkError( "VALIDATION", `filter: ${error instanceof Error ? error.message : "Invalid predicate"}` ); } }; const resolveSdkProjectAffinity = async ( config: Config, projectHints: readonly string[] | undefined ) => { try { return await resolveRemoteProjectAffinity(config, projectHints); } catch (error) { if (error instanceof ProjectAffinityInputError) { throw sdkError("VALIDATION", error.message); } throw error; } }; function unwrapStore( result: StoreResult, code: "STORE" | "RUNTIME" = "STORE" ): T { if (!result.ok) { throw sdkError(code, result.error.message, { cause: result.error.cause }); } return result.value; } function unwrapTraceStore(result: StoreResult): T { if (result.ok) return result.value; const code = result.error.code === "NOT_FOUND" ? "NOT_FOUND" : result.error.code === "INVALID_INPUT" || result.error.code === "CONSTRAINT_VIOLATION" ? "VALIDATION" : "STORE"; throw sdkError(code, result.error.message, { cause: result.error.cause, details: { traceCode: result.error.code }, }); } function unwrapKnowledgeDelta(result: KnowledgeDeltaServiceResult): T { if (result.success) return result.data; throw sdkError(result.isValidation ? "VALIDATION" : "STORE", result.error); } async function resolveClientState( options: GnoClientInitOptions = {} ): Promise { if (options.config && options.configPath) { throw sdkError("VALIDATION", "Pass either config or configPath, not both"); } let config: Config; let configPath: string | null; let configSource: "file" | "inline"; if (options.config) { const parsed = ConfigSchema.safeParse(options.config); if (!parsed.success) { throw sdkError( "CONFIG", parsed.error.issues[0]?.message ?? "Invalid config" ); } config = normalizeConfigContentTypes(parsed.data).config; configPath = null; configSource = "inline"; } else { const loaded = await loadConfig(options.configPath); if (!loaded.ok) { throw sdkError("CONFIG", loaded.error.message); } config = loaded.value; configPath = options.configPath ?? null; configSource = "file"; } const indexName = canonicalizeIndexName( options.indexName ?? DEFAULT_INDEX_NAME ); const dbPath = options.dbPath ?? getIndexDbPath(indexName); await mkdir(dirname(dbPath), { recursive: true }); const store = new SqliteAdapter(); store.setConfigPath(configPath ?? ""); unwrapStore( await store.open(dbPath, config.ftsTokenizer, config.busyTimeoutMs) ); unwrapStore(await store.syncCollections(config.collections)); unwrapStore(await store.syncContexts(config.contexts ?? [])); return { config, configPath, configSource, dbPath, store, llm: new LlmAdapter(config, options.cacheDir), downloadPolicy: options.downloadPolicy ?? resolveDownloadPolicy(process.env, {}), indexName, }; } /** SDK error family per memory code; exhaustive so a new code fails to compile. */ const MEMORY_ERROR_TO_SDK: Readonly> = { MEMORY_TEXT_REQUIRED: "VALIDATION", MEMORY_TEXT_TOO_LARGE: "VALIDATION", MEMORY_QUERY_REQUIRED: "VALIDATION", MEMORY_BUDGET_INVALID: "VALIDATION", MEMORY_COLLECTION_REQUIRED: "VALIDATION", MEMORY_COLLECTION_NOT_FOUND: "NOT_FOUND", MEMORY_COLLECTION_UNMANAGED: "VALIDATION", MEMORY_SCOPES_REQUIRED: "VALIDATION", MEMORY_SCOPES_INVALID: "VALIDATION", MEMORY_IDENTITY_REQUIRED: "VALIDATION", MEMORY_DECISION_INVALID: "VALIDATION", MEMORY_PREDECESSOR_REQUIRED: "VALIDATION", MEMORY_PREDECESSOR_NOT_FOUND: "NOT_FOUND", MEMORY_PREDECESSOR_HASH_MISMATCH: "RUNTIME", MEMORY_SUPERSEDE_CONFLICT: "RUNTIME", MEMORY_SUPERSEDE_PROJECTION_FAILED: "RUNTIME", MEMORY_FENCED_REPLAY: "VALIDATION", MEMORY_FENCED_DERIVED: "VALIDATION", MEMORY_WRITE_LEASE_BUSY: "RUNTIME", MEMORY_SYNC_FAILED: "RUNTIME", MEMORY_QUERY_FAILED: "RUNTIME", }; /** Map a core MemoryError onto the SDK error family; the memory code survives in `details.code`. */ function toMemorySdkError(cause: unknown): unknown { if (!(cause instanceof MemoryError)) return cause; return sdkError(MEMORY_ERROR_TO_SDK[cause.code], cause.message, { cause, details: { code: cause.code }, }); } class GnoClientImpl implements GnoClient { config: Config; readonly dbPath: string; readonly configPath: string | null; readonly configSource: "file" | "inline"; private readonly store: SqliteAdapter; private readonly llm: LlmAdapter; private readonly downloadPolicy: DownloadPolicy; private readonly indexName: string; private closed = false; constructor(state: OpenedClientState) { this.config = state.config; this.dbPath = state.dbPath; this.configPath = state.configPath; this.configSource = state.configSource; this.store = state.store; this.llm = state.llm; this.downloadPolicy = state.downloadPolicy; this.indexName = state.indexName; } isOpen(): boolean { return !this.closed && this.store.isOpen(); } private assertOpen(): void { if (!this.isOpen()) { throw sdkError("RUNTIME", "GNO client is closed"); } } private getCollections(collection?: string) { if (!collection) { return this.config.collections; } const filtered = this.config.collections.filter( (c) => c.name === collection ); if (filtered.length === 0) { throw sdkError("VALIDATION", `Collection not found: ${collection}`); } return filtered; } private requireRefactorApplyConfirmation(options: { schemaVersion?: unknown; planDigest?: unknown; confirmation?: unknown; }) { const parsed = parseRefactorApplyConfirmation(options); if ("error" in parsed) { throw sdkError("VALIDATION", parsed.message); } return parsed; } private async prepareRefactorSource(ref: string) { const doc = await getDocumentByRef(this.store, this.config, ref, {}); const stored = await this.store.getDocumentByUri(doc.uri); if (!stored.ok || !stored.value) { throw sdkError("NOT_FOUND", "Document not found"); } const storedDoc = stored.value; const collection = this.getCollections(storedDoc.collection)[0]; if (!collection) { throw sdkError( "VALIDATION", `Collection not found: ${storedDoc.collection}` ); } const capabilities = getDocumentCapabilities({ sourceExt: storedDoc.sourceExt, sourceMime: storedDoc.sourceMime, contentAvailable: storedDoc.mirrorHash !== null, recordKey: storedDoc.recordKey, }); if (!capabilities.editable) { throw sdkError( "VALIDATION", capabilities.reason ?? "Document is read-only in place." ); } return { storedDoc, collection, sourceFullPath: `${collection.path}/${storedDoc.relPath}`, editable: capabilities.editable, }; } private async createRuntimePorts(options: { embed?: boolean; expand?: boolean; answer?: boolean; rerank?: boolean; collection?: string; requiredEmbed?: boolean; requiredExpand?: boolean; requiredAnswer?: boolean; requiredRerank?: boolean; embedModel?: string; expandModel?: string; genModel?: string; rerankModel?: string; }): Promise { this.assertOpen(); const egressCollections = options.collection ? [options.collection] : ("all" as const); let embedPort: EmbeddingPort | null = null; let expandPort: GenerationPort | null = null; let answerPort: GenerationPort | null = null; let rerankPort: RerankPort | null = null; let vectorIndex: VectorIndexPort | null = null; if (options.embed) { const embedResult = await this.llm.createEmbeddingPort( resolveModelUri( this.config, "embed", options.embedModel, options.collection ), { egressCollections, policy: this.downloadPolicy, } ); if (embedResult.ok) { embedPort = embedResult.value; const initResult = await embedPort.init(); if (initResult.ok) { const vectorResult = await createVectorIndexPort( this.store.getRawDb(), { model: embedPort.modelUri, dimensions: embedPort.dimensions(), } ); if (vectorResult.ok) { vectorIndex = vectorResult.value; } else if (options.requiredEmbed) { await embedPort.dispose(); throw sdkError("STORE", vectorResult.error.message, { cause: vectorResult.error.cause, }); } } else if (options.requiredEmbed) { await embedPort.dispose(); throw sdkError("MODEL", initResult.error.message, { cause: initResult.error.cause, }); } } else if (options.requiredEmbed) { throw sdkError("MODEL", embedResult.error.message, { cause: embedResult.error.cause, }); } } if (options.expand) { const genResult = await this.llm.createExpansionPort( resolveModelUri( this.config, "expand", options.expandModel ?? options.genModel, options.collection ), { egressCollections, policy: this.downloadPolicy, } ); if (genResult.ok) { expandPort = genResult.value; } else if (options.requiredExpand) { if (embedPort) { await embedPort.dispose(); } throw sdkError("MODEL", genResult.error.message, { cause: genResult.error.cause, }); } } if (options.answer) { const genResult = await this.llm.createGenerationPort( resolveModelUri( this.config, "gen", options.genModel, options.collection ), { egressCollections, policy: this.downloadPolicy, } ); if (genResult.ok) { answerPort = genResult.value; } else if (options.requiredAnswer) { if (embedPort) { await embedPort.dispose(); } if (expandPort) { await expandPort.dispose(); } throw sdkError("MODEL", genResult.error.message, { cause: genResult.error.cause, }); } } if (options.rerank) { const rerankResult = await this.llm.createRerankPort( resolveModelUri( this.config, "rerank", options.rerankModel, options.collection ), { egressCollections, policy: this.downloadPolicy, } ); if (rerankResult.ok) { rerankPort = rerankResult.value; } else if (options.requiredRerank) { if (embedPort) { await embedPort.dispose(); } if (expandPort) { await expandPort.dispose(); } if (answerPort) { await answerPort.dispose(); } throw sdkError("MODEL", rerankResult.error.message, { cause: rerankResult.error.cause, }); } } return { embedPort, expandPort, answerPort, rerankPort, vectorIndex }; } private async disposeRuntimePorts(ports: RuntimePorts): Promise { return finishInferenceCleanup(() => this.disposeRuntimePortsOwned(ports)); } private async disposeRuntimePortsOwned(ports: RuntimePorts): Promise { if (ports.embedPort) { await ports.embedPort.dispose(); } if (ports.expandPort) { await ports.expandPort.dispose(); } if (ports.answerPort) { await ports.answerPort.dispose(); } if (ports.rerankPort) { await ports.rerankPort.dispose(); } } private decorateSearchResults(results: SearchResults): SearchResults { return { ...results, results: results.results.map((result) => ({ ...result, uri: decorateUriForIndex(result.uri, this.indexName), })), }; } async search( query: string, options: GnoSearchOptions = {} ): Promise { return withInferenceScope(options, () => this.searchRequest(query, options) ); } private async searchRequest( query: string, options: GnoSearchOptions ): Promise { this.assertOpen(); let traceSession: RetrievalTraceSession | null = null; try { const { projectHints, ...searchOptions } = options; if (searchOptions.filter !== undefined) searchOptions.filter = normalizeSdkMetadataFilter(searchOptions.filter); const projectAffinity = await resolveSdkProjectAffinity( this.config, projectHints ); traceSession = unwrapStore( await startRetrievalTraceRequest({ store: this.store, config: this.config, query, filters: retrievalTraceFilters(searchOptions), pipeline: "bm25", indexName: this.indexName, }) ); return attachRetrievalTraceMetadata( this.decorateSearchResults( unwrapStore( await searchBm25(this.store, query, { ...searchOptions, projectAffinity, contentTypeRules: normalizeContentTypes( this.config.contentTypes ?? [] ).rules, traceSession: traceSession ?? undefined, }) ) ), traceSession ?? undefined ); } catch (cause) { await finishRetrievalTraceAfterError(traceSession, cause); throw cause; } } async vsearch( query: string, options: GnoVectorSearchOptions = {} ): Promise { return withInferenceScope(options, () => this.vsearchRequest(query, options) ); } private async vsearchRequest( query: string, options: GnoVectorSearchOptions ): Promise { this.assertOpen(); let ports: RuntimePorts | null = null; let traceSession: RetrievalTraceSession | null = null; try { const { projectHints, ...searchOptions } = options; if (searchOptions.filter !== undefined) searchOptions.filter = normalizeSdkMetadataFilter(searchOptions.filter); const projectAffinity = await resolveSdkProjectAffinity( this.config, projectHints ); const embedUri = resolveModelUri( this.config, "embed", options.model, options.collection ); traceSession = unwrapStore( await startRetrievalTraceRequest({ store: this.store, config: this.config, query, filters: retrievalTraceFilters(searchOptions), pipeline: "vector", indexName: this.indexName, modelUris: [embedUri], }) ); ports = await this.createRuntimePorts({ embed: true, requiredEmbed: true, embedModel: options.model, collection: options.collection, }); if (!ports.embedPort || !ports.vectorIndex) { throw sdkError( "MODEL", "Vector search requires an embedding model and vector index" ); } const queryEmbedResult = await ports.embedPort.embed( formatQueryForEmbedding(query, ports.embedPort.modelUri) ); if (!queryEmbedResult.ok) { throw sdkError("MODEL", queryEmbedResult.error.message, { cause: queryEmbedResult.error.cause, }); } return attachRetrievalTraceMetadata( this.decorateSearchResults( unwrapStore( await searchVectorWithEmbedding( { store: this.store, vectorIndex: ports.vectorIndex, embedPort: ports.embedPort, config: this.config, }, query, new Float32Array(queryEmbedResult.value), { ...searchOptions, projectAffinity, traceSession: traceSession ?? undefined, } ) ) ), traceSession ?? undefined ); } catch (cause) { await finishRetrievalTraceAfterError(traceSession, cause); throw cause; } finally { if (ports) await this.disposeRuntimePorts(ports); } } async query( query: string, options: GnoQueryOptions = {} ): Promise { return withInferenceScope(options, () => this.queryRequest(query, options)); } private async queryRequest( query: string, options: GnoQueryOptions ): Promise { this.assertOpen(); const normalizedInput = normalizeStructuredQueryInput( query, options.queryModes ?? [] ); if (!normalizedInput.ok) { throw sdkError("VALIDATION", normalizedInput.error.message); } query = normalizedInput.value.query; options = { ...options, queryModes: normalizedInput.value.queryModes.length > 0 ? normalizedInput.value.queryModes : undefined, }; const expandRequested = !options.noExpand && !options.queryModes?.length; const rerankRequested = !options.noRerank; const embedUri = resolveModelUri( this.config, "embed", options.embedModel, options.collection ); const expandUri = expandRequested ? resolveModelUri( this.config, "expand", options.expandModel ?? options.genModel, options.collection ) : undefined; const rerankUri = rerankRequested ? resolveModelUri( this.config, "rerank", options.rerankModel, options.collection ) : undefined; let ports: RuntimePorts | null = null; let traceSession: RetrievalTraceSession | null = null; try { const { projectHints, ...queryOptions } = options; if (queryOptions.filter !== undefined) queryOptions.filter = normalizeSdkMetadataFilter(queryOptions.filter); const projectAffinity = await resolveSdkProjectAffinity( this.config, projectHints ); traceSession = unwrapStore( await startRetrievalTraceRequest({ store: this.store, config: this.config, query, filters: retrievalTraceFilters(queryOptions), pipeline: "hybrid", indexName: this.indexName, modelUris: [embedUri, expandUri, rerankUri].filter( (value): value is string => Boolean(value) ), }) ); ports = await this.createRuntimePorts({ embed: true, expand: expandRequested, rerank: rerankRequested, embedModel: options.embedModel, expandModel: options.expandModel, genModel: options.genModel, rerankModel: options.rerankModel, collection: options.collection, }); return attachRetrievalTraceMetadata( this.decorateSearchResults( unwrapStore( await searchHybrid( { store: this.store, config: this.config, vectorIndex: ports.vectorIndex, embedPort: ports.embedPort, expandPort: ports.expandPort, rerankPort: ports.rerankPort, }, query, { ...queryOptions, projectAffinity, traceSession: traceSession ?? undefined, } ) ) ), traceSession ?? undefined ); } catch (cause) { await finishRetrievalTraceAfterError(traceSession, cause); throw cause; } finally { if (ports) await this.disposeRuntimePorts(ports); } } async ask(query: string, options: GnoAskOptions = {}): Promise { return withInferenceScope(options, () => this.askRequest(query, options)); } private async askRequest( query: string, options: GnoAskOptions ): Promise { this.assertOpen(); const normalizedInput = normalizeStructuredQueryInput( query, options.queryModes ?? [] ); if (!normalizedInput.ok) { throw sdkError("VALIDATION", normalizedInput.error.message); } query = normalizedInput.value.query; options = { ...options, queryModes: normalizedInput.value.queryModes.length > 0 ? normalizedInput.value.queryModes : undefined, }; const verificationRequested = options.verify === true; const answerRequested = verificationRequested || Boolean(options.answer && !options.noAnswer); const needsExpansionGen = !verificationRequested && !options.noExpand && !options.queryModes?.length; const rerankRequested = !options.noRerank; const embedUri = resolveModelUri( this.config, "embed", options.embedModel, options.collection ); const expandUri = needsExpansionGen ? resolveModelUri( this.config, "expand", options.expandModel ?? options.genModel, options.collection ) : undefined; const answerUri = answerRequested ? resolveModelUri( this.config, "gen", options.genModel, options.collection ) : undefined; const rerankUri = rerankRequested ? resolveModelUri( this.config, "rerank", options.rerankModel, options.collection ) : undefined; let ports: RuntimePorts | null = null; let traceSession: RetrievalTraceSession | null = null; const hydration = new RequestHydration(this.store); try { const { projectHints, ...askOptions } = options; if (askOptions.filter !== undefined) askOptions.filter = normalizeSdkMetadataFilter(askOptions.filter); const projectAffinity = await resolveSdkProjectAffinity( this.config, projectHints ); traceSession = unwrapStore( await startRetrievalTraceRequest({ store: this.store, config: this.config, query, filters: retrievalTraceFilters(askOptions), pipeline: "ask", indexName: this.indexName, modelUris: [embedUri, expandUri, answerUri, rerankUri].filter( (value): value is string => Boolean(value) ), }) ); ports = await this.createRuntimePorts({ embed: true, expand: needsExpansionGen, answer: answerRequested, rerank: rerankRequested, expandModel: options.expandModel, genModel: options.genModel, embedModel: options.embedModel, rerankModel: options.rerankModel, collection: options.collection, }); if (answerRequested && !ports.answerPort) { await traceSession?.recordCapability( "answer_generation", "unavailable", "model_unavailable" ); await traceSession?.finish("failed"); throw sdkError( "MODEL", "Answer generation requested but no generation model is available" ); } if (verificationRequested && ports.answerPort) { const verified = await buildVerifiedAsk( query, { ...askOptions, projectAffinity }, { store: this.store, hydration, config: this.config, indexName: this.indexName, vectorIndex: ports.vectorIndex, embedPort: ports.embedPort, rerankPort: ports.rerankPort, genPort: ports.answerPort, projectAffinity, traceSession: traceSession ?? undefined, } ); if (traceSession) { unwrapStore( await traceSession.finish( answerTraceTerminalStatus(verified.citations) ) ); } return attachRetrievalTraceMetadata( verified, traceSession ?? undefined ); } const searchResult = unwrapStore( await searchHybrid( { store: this.store, hydration, config: this.config, vectorIndex: ports.vectorIndex, embedPort: ports.embedPort, expandPort: ports.expandPort, rerankPort: ports.rerankPort, }, query, { limit: options.limit, collection: options.collection, lang: options.lang, intent: options.intent, since: options.since, until: options.until, categories: options.categories, author: options.author, filter: askOptions.filter, tagsAll: options.tagsAll, tagsAny: options.tagsAny, exclude: options.exclude, minScore: options.minScore, graph: options.graph, noGraph: options.noGraph, queryModes: options.queryModes, noExpand: options.noExpand, noRerank: options.noRerank, candidateLimit: options.candidateLimit, explain: options.explain, queryLanguageHint: options.queryLanguageHint, projectAffinity, traceSession: traceSession ?? undefined, } ) ); let answer: string | undefined; let citations: AskResult["citations"]; let answerContext: AskResult["meta"]["answerContext"]; let answerGenerated = false; if ( answerRequested && ports.answerPort && searchResult.results.length > 0 ) { await traceSession?.recordCapability("answer_generation", "attempted"); const rawAnswer = await generateGroundedAnswer( { genPort: ports.answerPort, store: this.store, hydration }, query, searchResult.results, options.maxAnswerTokens ?? 512 ); if (!rawAnswer) { await traceSession?.recordCapability( "answer_generation", "failed", "generation_failed" ); await traceSession?.finish("failed"); throw sdkError("MODEL", "Answer generation failed"); } await traceSession?.recordCapability("answer_generation", "used"); const processed = await processAnswerResultWithTrace( rawAnswer, traceSession ?? undefined ); answer = processed.answer; citations = processed.citations; answerContext = processed.answerContext; answerGenerated = true; } const askResult: AskResult = { query, mode: searchResult.meta.vectorsUsed ? "hybrid" : "bm25_only", queryLanguage: searchResult.meta.queryLanguage ?? "und", answer, citations, results: searchResult.results, meta: { expanded: searchResult.meta.expanded ?? false, reranked: searchResult.meta.reranked ?? false, vectorsUsed: searchResult.meta.vectorsUsed ?? false, intent: searchResult.meta.intent, candidateLimit: searchResult.meta.candidateLimit, exclude: searchResult.meta.exclude, queryModes: searchResult.meta.queryModes, answerGenerated, totalResults: searchResult.results.length, answerContext, ...(options.explain && searchResult.meta.explain ? { explain: searchResult.meta.explain } : {}), }, }; if (answerRequested && traceSession) { if (!answerGenerated) { unwrapStore( await traceSession.recordCapability( "answer_generation", "unavailable", "no_evidence" ) ); } unwrapStore( await traceSession.finish(answerTraceTerminalStatus(citations)) ); } return attachRetrievalTraceMetadata(askResult, traceSession ?? undefined); } catch (cause) { await finishRetrievalTraceAfterError(traceSession, cause); throw cause; } finally { hydration.release(); if (ports) await this.disposeRuntimePorts(ports); } } async context(input: GnoContextInput): Promise { this.assertOpen(); const { projectHints, ...contextInput } = input; if (contextInput.filter !== undefined) contextInput.filter = normalizeSdkMetadataFilter(contextInput.filter); validateContextCapsuleBuildInput( { ...contextInput, indexName: this.indexName }, this.indexName, this.config.collections.map((collection) => collection.name) ); const collection = input.collections?.length === 1 ? input.collections[0] : undefined; const useModels = input.depthPolicy !== "fast"; const modelUris = useModels ? [ resolveModelUri(this.config, "embed", undefined, collection), resolveModelUri(this.config, "rerank", undefined, collection), ] : []; let ports: RuntimePorts | null = null; let traceSession: RetrievalTraceSession | null = null; try { const projectAffinity = await resolveSdkProjectAffinity( this.config, projectHints ); traceSession = unwrapStore( await startRetrievalTraceRequest({ store: this.store, config: this.config, query: input.query ?? input.goal, goal: input.goal, filters: { limit: input.limit, collection, collections: [...(input.collections ?? [])].sort(), lang: input.lang, tagsAll: input.tagsAll, tagsAny: input.tagsAny, since: input.since, until: input.until, categories: input.categories, author: input.author, filter: contextInput.filter, graph: input.graph, candidateLimit: input.candidateLimit, queryModes: input.queryModes, uriPrefix: input.uriPrefix ?? undefined, }, pipeline: "context", indexName: this.indexName, modelUris, }) ); ports = await this.createRuntimePorts({ embed: useModels, rerank: useModels, collection, }); const capsule = await buildContextCapsule( { ...contextInput, indexName: this.indexName }, { store: this.store, config: this.config, indexName: this.indexName, vectorIndex: ports.vectorIndex, embedPort: ports.embedPort, rerankPort: ports.rerankPort, projectAffinity, traceSession: traceSession ?? undefined, } ); if (traceSession) unwrapStore(await traceSession.finish("completed")); return attachRetrievalTraceMetadata(capsule, traceSession ?? undefined); } catch (cause) { await finishRetrievalTraceAfterError(traceSession, cause); throw cause; } finally { if (ports) await this.disposeRuntimePorts(ports); } } async verifyContext( capsule: GnoContextResult ): Promise { this.assertOpen(); return verifyContextCapsuleRuntime(capsule, { store: this.store, config: this.config, indexName: this.indexName, }); } async get(ref: string, options: GnoGetOptions = {}) { this.assertOpen(); const resolution = resolveEffectiveIndex([ref], this.indexName); if (!resolution.ok) { throw sdkError("VALIDATION", resolution.error); } const scoped = await openScopedIndexStore({ activeStore: this.store, activeIndexName: this.indexName, requestedIndexName: resolution.value.indexName, config: this.config, configPath: this.configPath, }); try { const result = await getDocumentByRef( scoped.store, this.config, ref, options ); const decorated = { ...result, uri: decorateUriForIndex(result.uri, scoped.indexName), }; const traceMetadata = getRetrievalTraceMetadata(result); if (traceMetadata) { Object.defineProperty(decorated, RETRIEVAL_TRACE_METADATA, { configurable: false, enumerable: false, value: traceMetadata, writable: false, }); } return decorated; } finally { await scoped.close(); } } async multiGet(refs: string[], options: GnoMultiGetOptions = {}) { this.assertOpen(); const resolution = resolveEffectiveIndex(refs, this.indexName); if (!resolution.ok) { throw sdkError("VALIDATION", resolution.error); } const scoped = await openScopedIndexStore({ activeStore: this.store, activeIndexName: this.indexName, requestedIndexName: resolution.value.indexName, config: this.config, configPath: this.configPath, }); try { const result = await multiGetDocuments( scoped.store, this.config, refs, options ); return { ...result, documents: result.documents.map((doc) => ({ ...doc, uri: decorateUriForIndex(doc.uri, scoped.indexName), })), }; } finally { await scoped.close(); } } async list(options: GnoListOptions = {}) { this.assertOpen(); const result = await listDocuments(this.store, options); return { ...result, documents: result.documents.map((doc) => ({ ...doc, uri: decorateUriForIndex(doc.uri, this.indexName), })), }; } async changes( options: ListKnowledgeChangesInput = {} ): Promise { this.assertOpen(); return unwrapKnowledgeDelta( await listKnowledgeChanges(this.store, options) ); } async diff(ref: string, changeId?: string): Promise { this.assertOpen(); return unwrapKnowledgeDelta( await getKnowledgeDiff(this.store, ref, changeId) ); } async impact( ref: string, options: KnowledgeImpactInput = {} ): Promise { this.assertOpen(); return unwrapKnowledgeDelta( await analyzeKnowledgeImpact(this.store, ref, options) ); } async status(): Promise { this.assertOpen(); const status = unwrapStore( await this.store.getStatus({ embedModel: resolveModelUri(this.config, "embed"), chunking: this.config.chunking ?? {}, }) ); return { ...status, contentTypeBoost: buildContentTypeBoostStatus( this.config.contentTypes ?? [] ), }; } async listRetrievalTraces( options: import("../core/retrieval-trace-management").RetrievalTraceListRequest = {} ) { this.assertOpen(); return unwrapTraceStore( await new RetrievalTraceManagementService(this.store).list(options) ); } async getRetrievalTrace( traceId: string, options: { detailLimit?: number } = {} ) { this.assertOpen(); return unwrapTraceStore( await new RetrievalTraceManagementService(this.store).show( traceId, options ) ); } async labelRetrievalTrace( input: import("../core/retrieval-trace-management").RetrievalTraceLabelRequest ) { this.assertOpen(); return unwrapTraceStore( await new RetrievalTraceManagementService(this.store).label(input) ); } async exportRetrievalTraces( input: import("../core/retrieval-trace-management").RetrievalTraceExportRequest ) { this.assertOpen(); return unwrapTraceStore( await new RetrievalTraceManagementService(this.store, { authorizeExport: async (lineage) => { return authorizeCurrentEgress({ store: this.store, config: this.config, lineage, action: "export", destinationZone: "local_process", caller: { authenticated: true, operationAuthorized: true }, contentClass: "retrieval_trace", }); }, }).export(input) ); } async deleteRetrievalTrace(traceId: string) { this.assertOpen(); return unwrapTraceStore( await new RetrievalTraceManagementService(this.store).delete(traceId) ); } async purgeRetrievalTraces() { this.assertOpen(); return unwrapTraceStore( await new RetrievalTraceManagementService(this.store).purge() ); } async getCollectionEgressPolicy(collection: string) { this.assertOpen(); const state = new CollectionEgressPolicyService({ getConfig: () => this.config, }).get(collection); if (!state.ok) { throw sdkError( state.code === "NOT_FOUND" ? "NOT_FOUND" : "VALIDATION", state.error ); } return state.value; } async setCollectionEgressPolicy( collection: string, policy: import("../config/types").EgressPolicy, confirmation?: import("../core/collection-egress-policy-service").EgressRelaxationConfirmation ) { this.assertOpen(); if (!this.configPath) { throw sdkError( "VALIDATION", "Inline-config clients cannot persist collection policy changes" ); } const service = new CollectionEgressPolicyService({ getConfig: () => this.config, mutateConfig: (mutate) => applyConfigChange( { store: this.store, configPath: this.configPath ?? undefined, onConfigUpdated: (config) => { this.config = config; }, projectStore: (store, config) => projectCollectionEgressPolicy(store, config, collection), }, mutate ), }); const result = await service.set({ collection, policy, confirmation, }); if (!result.ok) throw sdkError("VALIDATION", result.error); return result.value; } async checkEgress( input: import("../core/collection-egress-policy-service").CollectionEgressCheckInput ) { this.assertOpen(); const result = new CollectionEgressPolicyService({ getConfig: () => this.config, }).check(input); if (!result.ok) throw sdkError("VALIDATION", result.error); return result.value; } async listEgressAudits(options: { limit?: number; cursor?: string } = {}) { this.assertOpen(); return unwrapTraceStore( await new EgressAuditService(this.store).list(options) ); } async getEgressAudit(auditId: string) { this.assertOpen(); return unwrapTraceStore( await new EgressAuditService(this.store).show(auditId) ); } async getEgressAuditStatus() { this.assertOpen(); return unwrapTraceStore(await new EgressAuditService(this.store).status()); } async deleteEgressAudit(auditId: string) { this.assertOpen(); return unwrapTraceStore( await new EgressAuditService(this.store).delete(auditId) ); } async purgeEgressAudits() { this.assertOpen(); return unwrapTraceStore(await new EgressAuditService(this.store).purge()); } async update(options: GnoUpdateOptions = {}): Promise { this.assertOpen(); const collections = this.getCollections(options.collection); return defaultSyncService.syncAll( collections, this.store, withContentTypeRules( { gitPull: options.gitPull, runUpdateCmd: true, }, this.config ) ); } async embed(options: GnoEmbedOptions = {}): Promise { this.assertOpen(); return runEmbed( { config: this.config, store: this.store, llm: this.llm, downloadPolicy: this.downloadPolicy, }, options ); } async index(options: GnoIndexOptions = {}): Promise { const syncResult = await this.update(options); if (options.noEmbed) { return { syncResult, embedSkipped: true }; } const embedResult = await this.embed(options); return { syncResult, embedSkipped: false, embedResult, }; } async createNote( options: GnoCreateNoteOptions ): Promise { this.assertOpen(); const collection = this.getCollections(options.collection)[0]; if (!collection) { throw sdkError( "VALIDATION", `Collection not found: ${options.collection}` ); } const existingList = await this.store.listDocuments(collection.name); if (!existingList.ok) { throw sdkError("STORE", existingList.error.message, { cause: existingList.error.cause, }); } const plan = resolveNoteCreatePlan( { collection: collection.name, title: options.title, relPath: options.relPath, folderPath: options.folderPath, collisionPolicy: options.collisionPolicy, }, existingList.value.map((doc) => doc.relPath) ); const fullPath = `${collection.path}/${plan.relPath}`; if (plan.openedExisting) { const existingDoc = await this.store.getDocument( collection.name, plan.relPath ); if (!existingDoc.ok || !existingDoc.value) { throw sdkError("NOT_FOUND", "Existing note could not be resolved"); } return { uri: existingDoc.value.uri, path: fullPath, relPath: plan.relPath, created: false, openedExisting: true, }; } const validatedTags = options.tags?.length ? parseAndValidateTagFilter(options.tags.join(",")) : []; const presetContent = resolveNotePreset({ presetId: options.presetId, title: options.title?.trim() || plan.filename.replace(/\.[^.]+$/u, "") || "Untitled", tags: validatedTags, body: options.content, }); let contentToWrite = presetContent?.content ?? options.content ?? `# ${options.title?.trim() || "Untitled"}\n`; if ( validatedTags.length > 0 && [".md", ".markdown"].includes( plan.filename.slice(plan.filename.lastIndexOf(".")).toLowerCase() ) ) { contentToWrite = updateFrontmatterTags(contentToWrite, validatedTags); } await mkdir(dirname(fullPath), { recursive: true }); await atomicWrite(fullPath, contentToWrite); const syncResults = await defaultSyncService.syncFiles( collection, this.store, [plan.relPath], withContentTypeRules( { runUpdateCmd: false, gitPull: false, }, this.config ) ); const syncResult = syncResults[0]; if (!syncResult || syncResult.status === "error") { throw sdkError( "RUNTIME", syncResult?.errorMessage ?? "Failed to sync created note" ); } return { uri: `gno://${collection.name}/${plan.relPath}`, path: fullPath, relPath: plan.relPath, created: true, openedExisting: false, createdWithSuffix: plan.createdWithSuffix, }; } /** * The memory service owns the shared write lease; the SDK never takes it. * Embedding is best-effort: without a local model the service reports * lexical-only matching/retrieval instead of failing. */ private async withMemoryService( collection: string | undefined, run: (service: MemoryService) => Promise ): Promise { this.assertOpen(); const ports = await this.createRuntimePorts({ embed: true, collection: this.config.collections.some( (candidate) => candidate.name === collection ) ? collection : undefined, }); try { return await run( new MemoryService({ store: this.store, config: this.config, collections: this.config.collections, lockPath: writeLeasePath(this.dbPath), embedPort: ports.embedPort, vectorIndex: ports.vectorIndex, }) ); } catch (cause) { throw toMemorySdkError(cause); } finally { await this.disposeRuntimePorts(ports); } } async remember(input: GnoRememberInput): Promise { return this.withMemoryService(input?.collection, (service) => service.remember(input) ); } async recall(input: GnoRecallInput): Promise { return this.withMemoryService(input?.collection, (service) => service.recall(input) ); } async capture(options: GnoCaptureOptions): Promise { this.assertOpen(); const collection = this.getCollections(options.collection)[0]; if (!collection) { throw sdkError( "VALIDATION", `Collection not found: ${options.collection}` ); } const existingList = await this.store.listDocuments(collection.name); if (!existingList.ok) { throw sdkError("STORE", existingList.error.message, { cause: existingList.error.cause, }); } const { overwrite: _unsupportedOverwrite, ...captureOptions } = options as GnoCaptureOptions & { overwrite?: unknown }; if (_unsupportedOverwrite !== undefined) { throw sdkError( "VALIDATION", "overwrite is not supported by client.capture(); use collisionPolicy instead" ); } let plan: CapturePlan; try { plan = planCapture({ input: { ...captureOptions, collection: collection.name, }, existingRelPaths: existingList.value.map((doc) => doc.relPath), diskRelPaths: await listCaptureDiskRelPaths(collection.path), }); } catch (error) { throw sdkError( "VALIDATION", error instanceof Error ? error.message : String(error) ); } const fullPath = `${collection.path}/${plan.relPath}`; if (plan.openedExisting) { const existingDoc = await this.store.getDocument( collection.name, plan.relPath ); if (!existingDoc.ok) { throw sdkError("STORE", existingDoc.error.message, { cause: existingDoc.error.cause, }); } return buildCaptureReceipt({ plan, absPath: fullPath, docid: existingDoc.value?.docid, sync: existingDoc.value ? { status: "completed" } : { status: "skipped", reason: "Existing file is not indexed yet.", }, }); } await mkdir(dirname(fullPath), { recursive: true }); await writeCapturePlanFile(plan, fullPath); const syncResults = await defaultSyncService.syncFiles( collection, this.store, [plan.relPath], withContentTypeRules( { runUpdateCmd: false, gitPull: false, }, this.config ) ); const syncResult = syncResults[0]; const docResult = await this.store.getDocument( collection.name, plan.relPath ); const docid = docResult.ok ? docResult.value?.docid : undefined; return buildCaptureReceipt({ plan, absPath: fullPath, docid: syncResult?.docid ?? docid, sync: syncResult?.status === "error" ? { status: "failed", error: syncResult.errorMessage ?? syncResult.errorCode ?? "Unknown sync error", } : { status: "completed" }, }); } async createFolder( options: GnoCreateFolderOptions ): Promise { this.assertOpen(); const collection = this.getCollections(options.collection)[0]; if (!collection) { throw sdkError( "VALIDATION", `Collection not found: ${options.collection}` ); } const folderPath = planCreateFolder({ parentPath: options.parentPath, name: options.name, }); const fullPath = `${collection.path}/${folderPath}`; await createFolderPath(fullPath); return { collection: collection.name, folderPath, path: fullPath, }; } async previewRenameNote( options: GnoRenameNoteOptions ): Promise { this.assertOpen(); const prepared = await this.prepareRefactorSource(options.ref); const target = resolveRenameTarget({ collection: prepared.collection.name, currentRelPath: prepared.storedDoc.relPath, nextName: options.name, }); return buildCanonicalRefactorPlan({ operation: "rename", doc: prepared.storedDoc, collection: prepared.collection, sourceFullPath: prepared.sourceFullPath, target, store: this.store, sourceEditable: prepared.editable, }); } async previewMoveNote( options: GnoMoveNoteOptions ): Promise { this.assertOpen(); const prepared = await this.prepareRefactorSource(options.ref); const target = resolveMoveTarget({ collection: prepared.collection.name, currentRelPath: prepared.storedDoc.relPath, folderPath: options.folderPath, nextName: options.name, }); return buildCanonicalRefactorPlan({ operation: "move", doc: prepared.storedDoc, collection: prepared.collection, sourceFullPath: prepared.sourceFullPath, target, store: this.store, sourceEditable: prepared.editable, }); } async renameNote( options: GnoRenameNoteApplyOptions ): Promise { this.assertOpen(); const confirmation = this.requireRefactorApplyConfirmation(options); const prepared = await this.prepareRefactorSource(options.ref); const target = resolveRenameTarget({ collection: prepared.collection.name, currentRelPath: prepared.storedDoc.relPath, nextName: options.name, }); const plan = await buildCanonicalRefactorPlan({ operation: "rename", doc: prepared.storedDoc, collection: prepared.collection, sourceFullPath: prepared.sourceFullPath, target, store: this.store, sourceEditable: prepared.editable, }); const apply = await applyCanonicalFileRefactor({ plan, confirmation, deps: buildDurableFileRefactorApplyDeps({ collection: prepared.collection, store: this.store, syncAfterCommit: async () => { const syncResult = await defaultSyncService.syncCollection( prepared.collection, this.store, withContentTypeRules({ runUpdateCmd: false }, this.config) ); assertFileRefactorSyncConverged(syncResult); }, }), }); return apply; } async moveNote( options: GnoMoveNoteApplyOptions ): Promise { this.assertOpen(); const confirmation = this.requireRefactorApplyConfirmation(options); const prepared = await this.prepareRefactorSource(options.ref); const target = resolveMoveTarget({ collection: prepared.collection.name, currentRelPath: prepared.storedDoc.relPath, folderPath: options.folderPath, nextName: options.name, }); const plan = await buildCanonicalRefactorPlan({ operation: "move", doc: prepared.storedDoc, collection: prepared.collection, sourceFullPath: prepared.sourceFullPath, target, store: this.store, sourceEditable: prepared.editable, }); const apply = await applyCanonicalFileRefactor({ plan, confirmation, deps: buildDurableFileRefactorApplyDeps({ collection: prepared.collection, store: this.store, syncAfterCommit: async () => { const syncResult = await defaultSyncService.syncCollection( prepared.collection, this.store, withContentTypeRules({ runUpdateCmd: false }, this.config) ); assertFileRefactorSyncConverged(syncResult); }, }), }); return apply; } async duplicateNote( options: GnoDuplicateNoteOptions ): Promise { this.assertOpen(); const doc = await getDocumentByRef( this.store, this.config, options.ref, {} ); const stored = await this.store.getDocumentByUri(doc.uri); if (!stored.ok || !stored.value) { throw sdkError("NOT_FOUND", "Document not found"); } const storedDoc = stored.value; const collection = this.getCollections(storedDoc.collection)[0]; if (!collection) { throw sdkError( "VALIDATION", `Collection not found: ${storedDoc.collection}` ); } const docsResult = await this.store.listDocuments(collection.name); if (!docsResult.ok) { throw sdkError("STORE", docsResult.error.message, { cause: docsResult.error.cause, }); } const plan = planDuplicateRefactor({ collection: collection.name, currentRelPath: storedDoc.relPath, folderPath: options.folderPath, nextName: options.name, existingRelPaths: docsResult.value.map((entry) => entry.relPath), }); const currentPath = `${collection.path}/${storedDoc.relPath}`; const nextPath = `${collection.path}/${plan.nextRelPath}`; await mkdir(dirname(nextPath), { recursive: true }); await copyFilePath(currentPath, nextPath); await defaultSyncService.syncCollection( collection, this.store, withContentTypeRules({ runUpdateCmd: false }, this.config) ); const linksResult = await this.store.getLinksForDoc(storedDoc.id); const backlinksResult = await this.store.getBacklinksForDoc(storedDoc.id); if (!linksResult.ok || !backlinksResult.ok) { throw sdkError("STORE", "Failed to compute refactor warnings"); } return { uri: plan.nextUri, path: nextPath, relPath: plan.nextRelPath, warnings: buildRefactorWarnings({ backlinks: backlinksResult.value.length, wikiLinks: linksResult.value.filter( (entry) => entry.linkType === "wiki" ).length, markdownLinks: linksResult.value.filter( (entry) => entry.linkType === "markdown" ).length, }).warnings, }; } async getSections(ref: string) { this.assertOpen(); const document = await getDocumentByRef(this.store, this.config, ref, {}); return extractSections(document.content); } async createSectionTarget( ref: string, selector: SectionTargetCreateSelector ): Promise { this.assertOpen(); const parsed = parseSectionTargetCreateSelector(selector); if (!parsed.ok) { throw sdkError("VALIDATION", parsed.error); } const document = await getDocumentByRef(this.store, this.config, ref, {}); // Top-level response uri shares schema maxLength — reject before create. if (!isTransportBoundedCanonicalUri(document.uri)) { throw sdkError("VALIDATION", CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS); } // Canonical identity from stored document — never from caller. const target = await createSectionTargetCore({ content: document.content, uri: document.uri, ...parsed.value, }); if (!target) { const sections = extractSections(document.content); const matched = parsed.value.anchor !== undefined ? sections.some((section) => section.anchor === parsed.value.anchor) : sections.some((section) => section.line === parsed.value.line); if (!matched) { throw sdkError("NOT_FOUND", "Section not found"); } throw sdkError("VALIDATION", "Section target exceeds size bounds"); } return projectSectionTargetCreateResult(document.uri, target); } async resolveSectionTarget( ref: string, target: SectionTargetV1 ): Promise { this.assertOpen(); const parsed = parseSectionTargetV1(target); if (!parsed.ok) { throw sdkError("VALIDATION", parsed.error); } const document = await getDocumentByRef(this.store, this.config, ref, {}); // Top-level uri must fit schema — citation fail-closed does not repair it. if (!isTransportBoundedCanonicalUri(document.uri)) { throw sdkError("VALIDATION", CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS); } const resolution = await resolveSectionTargetCore({ content: document.content, target: parsed.value, uri: document.uri, }); return projectSectionTargetResolveResult(document.uri, resolution); } async close(): Promise { if (this.closed) { return; } this.closed = true; await this.store.close(); await this.llm.dispose(); } } export async function createGnoClient( options: GnoClientInitOptions = {} ): Promise { if (options.indexName !== undefined && !isValidIndexName(options.indexName)) { throw sdkError( "VALIDATION", `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.` ); } const state = await resolveClientState(options); return new GnoClientImpl(state); }