@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
JavaScript
/**
* 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 } };
};