UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

157 lines (156 loc) • 6.74 kB
/** * Source loading, default resolution, and generic validation. * * Required fields (id/title/summary/domain/integrations) come straight * from the authored entry; omitted optionals fall back to SDK defaults. This * covers only provider-neutral invariants (ids, required fields, enum values, * relationship integrity). Host-specific rules — configuration-key coverage, * taxonomy conflicts — are enforced by the host build BEFORE the manifest * reaches NeuroLink. Errors block indexing; warnings are surfaced but do not. */ import { decodeArray, decodeString } from "type-decoder/dist/index.js"; import { normalizeText } from "./normalize.js"; const SDK_DEFAULT_KIND = "text"; const SDK_DEFAULT_STATUS = "active"; const STATUSES = new Set(["draft", "active", "deprecated"]); const KINDS = new Set([ "concept", "text", "configuration", "tool", "procedure", "policy", "troubleshooting", ]); /** * Resolve one authored entry into a complete `NormalizedKnowledgeEntry`, * materializing every omitted optional (optional arrays to `[]`, `body` to "", * `kind` to "text", `status` to "active") so downstream code never re-checks * it. Authored arrays are cloned so the normalized snapshot is isolated from * caller-owned references. */ export const resolveEntry = (input, version) => ({ id: input.id, title: input.title, summary: input.summary, domain: input.domain, integrations: decodeArray(input.integrations, decodeString) ?? [], kind: input.kind ?? SDK_DEFAULT_KIND, status: input.status ?? SDK_DEFAULT_STATUS, body: input.body ?? "", aliases: [...(input.aliases ?? [])], keywords: [...(input.keywords ?? [])], relatedEntryIds: [...(input.relatedEntryIds ?? [])], parentEntryId: input.parentEntryId, version, }); /** Expand a build manifest into sources — one per catalog. */ export const manifestToSources = (manifest) => manifest.catalogs.map((catalog) => ({ id: catalog.id, version: manifest.contentVersion, entries: catalog.entries, })); /** Resolve a source's entries and its effective version. */ const loadSourceEntries = (source, manifestVersion) => ({ id: source.id, version: source.version ?? manifestVersion, entries: source.entries, }); const error = (code, message, extra) => ({ level: "error", code, message, ...extra }); const warn = (code, message, extra) => ({ level: "warning", code, message, ...extra }); /** * Resolve and validate every source into normalized entries. `validation.ok` * is false when any error-level issue was found; the caller (index builder) * must not swap in an index built from an invalid set. Async so the public * contract is stable if a future source kind needs asynchronous loading. */ export const normalizeAndValidate = async (sources, options) => { const issues = []; const entries = []; const seenIds = new Set(); const aliasOwner = new Map(); for (const source of sources) { const loaded = loadSourceEntries(source, options.manifestVersion); if (!loaded.id) { issues.push(error("missing-source-id", "Source has no id")); } for (const input of loaded.entries) { if (!input.id) { issues.push(error("missing-entry-id", "Entry has no id", { sourceId: loaded.id })); continue; } if (seenIds.has(input.id)) { issues.push(error("duplicate-entry-id", `Duplicate entry id "${input.id}"`, { entryId: input.id, })); continue; } seenIds.add(input.id); if (!input.title || !input.title.trim()) { issues.push(error("missing-title", "Entry is missing a title", { entryId: input.id, })); } if (!input.summary || !input.summary.trim()) { issues.push(error("missing-summary", "Entry is missing a summary", { entryId: input.id, })); } if (!input.domain || !input.domain.trim()) { issues.push(error("missing-domain", "Entry is missing a domain", { entryId: input.id, })); } if (input.integrations === undefined) { issues.push(error("missing-integrations", "Entry is missing integrations", { entryId: input.id, })); } else if (decodeArray(input.integrations, decodeString) === null) { issues.push(error("bad-integrations", "Entry integrations must be an array of strings", { entryId: input.id })); } const normalized = resolveEntry(input, loaded.version); if (!KINDS.has(normalized.kind)) { issues.push(error("bad-kind", `Unsupported kind "${normalized.kind}"`, { entryId: normalized.id, })); } if (!STATUSES.has(normalized.status)) { issues.push(error("bad-status", `Unsupported status "${normalized.status}"`, { entryId: normalized.id, })); } for (const alias of normalized.aliases) { const key = normalizeText(alias); if (!key) { issues.push(warn("empty-alias", `Alias "${alias}" is empty after normalization`, { entryId: normalized.id })); continue; } const owner = aliasOwner.get(key); if (owner && owner !== normalized.id) { issues.push(warn("duplicate-alias", `Alias "${alias}" also used by "${owner}"`, { entryId: normalized.id })); } else { aliasOwner.set(key, normalized.id); } } entries.push(normalized); } } // Relationship integrity — resolved after all entries are collected. const idSet = new Set(entries.map((entry) => entry.id)); for (const entry of entries) { for (const related of entry.relatedEntryIds) { if (!idSet.has(related)) { issues.push(warn("broken-relation", `relatedEntryId "${related}" not found`, { entryId: entry.id, })); } } if (entry.parentEntryId && !idSet.has(entry.parentEntryId)) { issues.push(warn("broken-parent", `parentEntryId "${entry.parentEntryId}" not found`, { entryId: entry.id })); } } const ok = !issues.some((issue) => issue.level === "error"); return { entries, validation: { ok, issues } }; };