@exellix/narrix-detector
v2.0.0
Published
Generic narrative detection pipeline (CNI v1 + narrator mapping v1 + feature packs + rules + signals) for Node.js/TypeScript.
Readme
narrix-detector
A generic, deterministic narrative detection pipeline for Node.js/TypeScript.
It standardizes three things:
- CNI v11 — detector input shape:
cni(subject + facts + signals + evidence) + scoping (facts and signals from upstream, e.g. scoper) - Narrator Mapping v1 — used outside the detector (e.g. in narrix-scoper) to turn raw objects into CNI
- Detection pipeline — rules → signals/narratives (with conflict resolution)
This package is intentionally generic: you bring your domain packs (rules, signal catalog). Normalization (mapping + raw input → CNI) is done by the caller or by a separate package (e.g. narrix-scoper).
Install
npm i @exellix/narrix-detectorPackage name: @exellix/narrix-detector (scoped npm package)
Optional peers (only needed if you want adapters):
npm i nx-functions nx-rulesQuick start
Detector input is CNI v11 + scoping. Build CniV11 and scoping: { facts, signals } yourself or use mappingToCni + toDetectorInput when you still have raw input + mapping.
import {
createNarrativeEngine,
toDetectorInput,
mappingToCni,
type CniV11,
type CniFactV11,
type CniSignalV11,
type SignalsCatalogV1,
type RulePackV1,
} from "@exellix/narrix-detector";
const signals: SignalsCatalogV1 = {
schema: "signals.catalog.v1",
signals: {
SIG_PUBLIC_EXPOSED: {
code: "SIG_PUBLIC_EXPOSED",
title: "Public exposure",
severityDefault: "medium",
description: "Subject appears to be exposed to untrusted scope.",
evidence: { expected: ["path:isPublic", "path:ingress.pathCount"] }
}
}
};
const rules: RulePackV1 = {
schema: "rules.pack.v1",
packId: "subnet.rules.v1",
version: "1.0.0",
rules: [
{
ruleId: "subnet-public-exposed",
when: { eq: [{ path: "baggage.subnet.isPublic" }, { const: true }] },
emit: {
signals: [
{ code: "SIG_PUBLIC_EXPOSED", severity: "medium", evidence: [{ path: "isPublic" }] }
],
narratives: [{ narrativeTypeId: "subnet.exposure.public-facing", confidence: 0.8 }]
}
}
]
};
const engine = createNarrativeEngine({
signalsCatalog: signals,
rulePacks: [rules],
});
// Example: normalize elsewhere (e.g. mappingToCni), then run detector
const mapping = { /* NarratorMappingV1 */ };
const rawInput = { id: "subnet-123", name: "Subnet A", cidr: "10.0.1.0/24", isPublic: true };
const { cni } = await mappingToCni(mapping, rawInput);
cni.baggage = cni.baggage ?? {};
(cni.baggage as any).subnet = { isPublic: rawInput.isPublic };
const result = await engine.evaluate(toDetectorInput(cni));
console.log(result.signals);
console.log(result.narratives);Or pass a pre-built CNI v11 + scoping (e.g. from narrix-scoper):
const detectorInput = {
cni: myCniV11, // CniV11
scoping: {
facts: myFacts, // CniFactV11[]
signals: mySignals // CniSignalV11[]
}
};
const result = await engine.evaluate(detectorInput);Detector input
The detector accepts a single shape:
cni—CniV11(subject, meta, optional facts/signals/narratives, baggage, etc.)scoping—{ facts: CniFactV11[]; signals: CniSignalV11[] }(facts and signals from upstream scope; merged withcniduring evaluation)
Use toDetectorInput(cniV1, scoping?) to build this from a CniV1 (e.g. from mappingToCni). Normalization (raw input + mapping → CNI) is not done inside the detector; use mappingToCni in your pipeline or get CNI from narrix-scoper.
Relationship to engine and outcome classification
- Story contracts — This package defines the canonical shapes for narratives and CNI (e.g.
CniNarrativeV1,narrativeTypeId,confidence,evidence,meta). Engine and consumers can rely on these types when integrating with narrix-detector. - Outcome classification — Mapping
narrativeTypeIdto outcome labels (e.g.outcomes[narrativeTypeId]) is engine-owned. The classifier pack format and outcome layer live in the engine (or consumer). narrix-detector does not define or require an outcome/classifier pack; it only emits narratives from rules. - Optional dependency — For runtimes that do not use narrix-detector for story contracts or pipeline, this package is optional (documentation-only). When the engine or consumers rely on CNI/narrative contracts or the detection pipeline, add
@exellix/narrix-detectoras a dependency and align with this repo’s specs and.reportschecklists.
Important contracts (pack authors)
Feature → Signal boundary
Features do not emit signals. Features can:
- compute derived values and write them to
baggage.* - add “candidates” to
baggage.candidates.*(optional pattern)
Only rules emit signals (and narratives). The detector does not run mapping-declared features; run them before calling the detector. See .reports/gap-analysis-detector-input-v11.md.
Conflict resolution scope (default)
Conflicts are resolved per (itemId + subject.id + signalKey). Default merge:
- keep max severity
- union evidence pointers
- union tags
You can override merge policy in createNarrativeEngine({ conflictPolicy }).
Advanced Features
Generalization (Semantic Layer)
Map source-specific paths to generic identifiers to write rules once:
"semantics": {
"$.risk.score": { "path": "metrics.threat_level" }
}Rules can then use when: { gt: [{ path: "$.risk.score" }, { const: 8 }] }.
Flexible Mappings
_iterate: Process arrays to generate multiple facts/bullets._condition: conditional logic (equals, startsWith, gte, etc.).bullets: Organize qualitative summaries into named sections.recommendations: Dynamic advice based on direct paths or conditional lists.
See SPEC.md for the full format specification.
Adapters
nx-functions adapter
import { createNxFunctionsRegistryAdapter } from "@exellix/narrix-detector/adapters/nx-functions";nx-rules adapter
import { createNxRulesRuleEngineAdapter } from "@exellix/narrix-detector/adapters/nx-rules";Version history
See CHANGELOG.md for release notes and contract-version compatibility.
License
MIT
