@exellix/narrix-runner
v2.0.0
Published
Thin orchestration between callers and the narrix pipeline: CNI + datasetId → pack resolution → engine → stories and signals
Readme
@exellix/narrix-runner
Thin orchestration glue between callers (e.g. @woroces/ai-tasks) and the narrix pipeline.
The runner always speaks CNI. Give it a CniV11 and a datasetId. It resolves the right pack, runs the engine, and returns stories and signals. What produced the CNI is not its concern.
CniV11 + datasetId
→ narrix-packs-library (resolve pack by datasetId)
→ narrix-engine (run pipeline)
→ { stories, signals }Install
npm i @exellix/narrix-runnerThree dependencies, nothing else: narrix-engine, narrix-packs-library, narrix-cni.
Quick start
import { createRunner } from "@exellix/narrix-runner";
import type { CniV11 } from "@exellix/narrix-cni";
// Create once — stateless, reuse across all calls
const runner = await createRunner();
const result = await runner.run({
cni: myCni, // caller is responsible for producing this
datasetId: "neo.vulnerabilities" // resolves the pack AND routes in the engine
});
console.log(result.stories); // all stories, flattened across passes
console.log(result.signals); // all signals, flattened across passes
console.log(result.passes.scoping); // { stories, signals } for the scoping pass
console.log(result.passes.discovery); // { stories, signals } for the discovery pass
console.log(result.entity.entityKey); // deterministic join keyInput contract
type RunnerInput = {
cni: CniV11; // always required
datasetId: string; // always required
processorId?: string; // optional — explicit processor override
sourceMeta?: SourceMeta;
};The runner has exactly one input shape. There is no kind field and no routing on input type. If you have unstructured data (text, chat, records) that isn't CNI yet, convert it to CniV11 before calling the runner. That conversion is not this package's responsibility.
Pack resolution
The runner calls findPackByDatasetId(datasetId) from @exellix/narrix-packs-library. The library finds the pack whose processor route.datasetIds includes the given datasetId. One datasetId maps to exactly one pack. You don't configure this — pass a datasetId and it works.
Fallback: pass the pack directly
For testing or edge cases where datasetId-based resolution isn't available:
const result = await runner.run({
cni: myCni,
datasetId: "test.dataset",
pack: myResolvedPack // bypasses library lookup
});Resolution priority: caller-provided pack → findPackByDatasetId(datasetId) → throws RUNNER_PACK_NOT_FOUND.
Result shape
type RunnerResult = {
stories: NarrativeStory[]; // flattened across all passes
signals: NarrativeSignal[]; // flattened across all passes
passes: Record<string, {
stories: NarrativeStory[];
signals: NarrativeSignal[];
}>;
entity: {
entityKind: string;
entityId?: string;
entityKey: string;
};
meta: {
runId: string;
producedAt: number;
processorId: string;
packId: string;
packVersion: string;
datasetId: string;
};
raw?: unknown; // only present when includeRaw: true
};stories and signals are the primary outputs — flattened for callers that don't need pass separation. passes gives per-pass access when you do. Facts are not in the default output; use includeRaw: true to get the full _narrix attachment.
Batch processing
const batchResult = await runner.runMany({
inputs: cniList.map(cni => ({ cni, datasetId: "neo.vulnerabilities" })),
onError: "attachError" // "throw" | "attachError" | "skip" — default: "attachError"
});
console.log(batchResult.meta); // { total, succeeded, failed, skipped }
for (const item of batchResult.results) {
if ("error" in item) {
console.error(item.code, item.message);
} else {
console.log(item.stories);
}
}Failed inputs produce a RunnerErrorResult in the results array instead of aborting the batch when onError: "attachError".
Full API
createRunner(options?)
const runner = await createRunner({
featureRegistry?: {
async execute(name: string, ctx: unknown): Promise<FeatureResult>
},
runtime?: {
onProcessorNotMatched?: "throw" | "attachError" | "skip",
onMissingMapping?: "throw" | "attachError",
deterministicSort?: boolean
}
});Create once per process. Reuse for all calls.
runner.run(options)
const result = await runner.run({
cni: CniV11,
datasetId: string,
processorId?: string,
sourceMeta?: SourceMeta,
pack?: NarrixPack, // optional fallback
includeRaw?: boolean // default: false
});runner.runMany(options)
const result = await runner.runMany({
inputs: RunnerInput[],
pack?: NarrixPack, // if provided, used for all inputs
onError?: "throw" | "attachError" | "skip",
includeRaw?: boolean
});Accessing the full engine output
const result = await runner.run({
cni: myCni,
datasetId: "neo.vulnerabilities",
includeRaw: true
});
// result.raw is the full enriched record with _narrix attachment
// typed as unknown — you own the shape if you use thisError codes
| Code | Meaning |
|---|---|
| RUNNER_PACK_NOT_FOUND | findPackByDatasetId returned nothing for the given datasetId |
| RUNNER_ENGINE_FAILED | Engine threw an unrecoverable error |
| RUNNER_PROCESSOR_NOT_MATCHED | No processor matched the input (propagated from engine) |
All errors are RunnerError extends Error with code, message, and optional cause.
Package relationships
caller (e.g. @woroces/ai-tasks)
│ produces CniV11 however it needs to
│
▼
@exellix/narrix-runner ← you are here
│
├── @exellix/narrix-packs-library
│ findPackByDatasetId(datasetId) → NarrixPack
│
└── @exellix/narrix-engine
runOne(cni, pack) → _narrix attachment
│
▼
{ stories, signals }Auth
Uses the token in your repo's .npmrc / user .npmrc. Do not paste tokens into code or docs.
License
Proprietary (internal).
