npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@exellix/narrix-scoper

v2.0.0

Published

Scoper: CNI v1.1 in → facts[] + signals[] out. Rule packs over subject/content/references. Optional legacy engine + narrator mapping.

Readme

@exellix/narrix-scoper

Scoper accepts CNI v1.1 as its only input and outputs facts[] and signals[]. No adapter selection, no dataset routing, no engine pipeline — the engine is responsible for those.


Scoper API (primary)

import { scope, type ScoperPack, type ScoperResult } from "@exellix/narrix-scoper";
import type { CniV11 } from "@exellix/narrix-cni";

const pack: ScoperPack = {
  schema: "scoper.pack.v1",
  packId: "my.pack",
  version: "1.0.0",
  rules: [
    {
      ruleId: "has-cve",
      when: { refsContain: { type: "cve" } },
      emit: { signals: [{ code: "SIG_VULN_PRESENT", severity: "high" }] },
    },
  ],
};

const result: ScoperResult = await scope(cni, pack);
// result.schema === "narrix.scoping.v1"
// result.facts  — sorted by (kind, value, evidence)
// result.signals — sorted by (code, severity, evidence)
// result.diagnostics?.matchedRuleIds
  • Input: scope(cni: CniV11, pack: ScoperPack)
  • Output: ScoperResult = { schema, facts, signals, diagnostics? }
  • Rules read only cni.subject, cni.content, cni.references, cni.meta (CNI paths). Use refsContain: { type, value?, role? } to match references.
  • Deterministic: facts and signals are sorted; no reliance on input order.
  • Evidence in output must reference CNI (path, span with contentId, or ref with refId). See @exellix/narrix-cni for EvidencePointerV11.

Reference vocabulary

Scoper can only scope what adapters emit. The minimum reference set that scoper rules may rely on is documented and enforced in tests. See reference-vocabulary.ts and exports:

| refKey | type / role | Description | |--------|-------------|-------------| | ref.ip.asset | ip / assetIp | Asset IP address | | ref.vuln.pluginId | pluginId / vulnId | Vulnerability plugin id | | ref.vuln.cve | cve / vulnId | CVE identifier | | ref.vuln.severity | severity | Severity level | | ref.egress.outsideHit | egress / egressOutside | Egress / internet-reachable hint | | ref.subnet.cidr | subnet.cidr | Subnet CIDR |

import { SCOPER_REFERENCE_VOCABULARY, isKnownReferenceType } from "@exellix/narrix-scoper";

Legacy: engine + mapping

This package also exposes the narrative engine and narrator mapping for pipelines that build CNI from raw input and then run rules. For “scoper only” use the scope() API above.


Terminology

  • Narrative Type — A question or dimension (e.g., "subnet.exposure.public-facing"). Defined by narrativeTypeId in rules.
  • Story (Narrative Instance) — The scoped answer for a specific entity+type. This is the assembled story instance produced by the engine for a given subject and narrative type.
  • Narrative Outcome — A classification bucket for that story (if present). Optional classification label applied to a story instance.

See Terminology Glossary for detailed definitions and examples.


Install

npm i @exellix/narrix-scoper

Optional peers (only needed if you use the legacy engine with adapters):

npm i nx-functions nx-rules

Development

  • Build: npm run build
  • Test: npm test
  • Typecheck: npm run typecheck
  • Lint: npm run lint

See API reference for the full exported surface.


API Naming

| Current name | New preferred name | Notes | | ------------ | ------------------ | ----- | | createNarrativeEngine() | createNarrativeEngine() | No change (engine creates stories) | | evaluate() | evaluate() | No change (evaluates and builds stories) | | result.narratives | result.stories | Array of Story instances (Narrative Instances) | | CniNarrativeV1 | CniNarrativeV1 | Type name (represents a Story) | | narrativeTypeId | narrativeTypeId | No change (identifies Narrative Type) | | emit.narratives | emit.narratives | Rule emit field (contains Story instances) |

Note: Current code uses narratives as the key name. In terminology:

  • narratives[] represents stories (Narrative Instances)
  • Each item has a narrativeTypeId identifying the Narrative Type
  • Optional outcome field would represent Narrative Outcome (not yet in schema)

Quick start (mapping + features + rules)

import {
  createNarrativeEngine,
  type NarratorMappingV1,
  type SignalsCatalogV1,
  type RulePackV1,
} from "@exellix/narrix-scoper";

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 mapping: NarratorMappingV1 = {
  schema: "narrator.mapping.v1",
  mappingId: "subnet.narrator.v1",
  version: "1.0.0",
  subject: {
    type: { const: "subnet" },
    id: { path: "id" },
    displayName: { path: "name" }
  },
  facts: [
    { kind: "FACT_SUBNET_CIDR", value: { path: "cidr" }, evidence: [{ path: "cidr" }] }
  ],
  // features may populate baggage that rules can reference later
  features: [
    { id: "net.subnet.isPublic", callFn: "net.subnet.isPublic", writeTo: "baggage.subnet.isPublic" }
  ]
};

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 }] // emits Story instances
      }
    }
  ]
};

const engine = createNarrativeEngine({
  signalsCatalog: signals,
  // feature registry is optional; you can also run path-only mappings
  featureRegistry: {
    async execute(name, ctx) {
      if (name === "net.subnet.isPublic") {
        const isPublic = Boolean((ctx.input as any)?.isPublic);
        return { passed: true, baggage: { value: isPublic } };
      }
      return { passed: false, baggage: { error: "unknown function" } };
    }
  },
  rulePacks: [rules],
});

const result = await engine.evaluate({
  input: { id: "subnet-123", name: "Subnet A", cidr: "10.0.1.0/24", isPublic: true },
  mapping
});

console.log(result.signals);
// Terminology mapping: result.narratives are stories (Narrative Instances)
console.log(result.narratives); // Array of Story instances for this subject

Inputs you can pass

  • Raw object + mapping (recommended):
    • { input: any, mapping: NarratorMappingV1 }
  • CNI directly (if you already normalized elsewhere):
    • { cni: CniV1 }

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 stories). This keeps signals auditable.

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 }).


Deprecations

The following naming conventions are being aligned with the clarified terminology model. Current code identifiers remain for backward compatibility, but documentation uses the preferred terms:

  • result.narratives → represents stories (Narrative Instances). The key name narratives is retained in code, but semantically these are Story instances.
  • emit.narratives → emits Story instances for the given Narrative Type.
  • "Detect narratives" / "narrative detection" → prefer "Build stories" / "Assemble stories" / "Story assembly"

Note: No runtime behavior changes are planned. This is a documentation and terminology alignment only.


Adapters

nx-functions adapter

import { createNxFunctionsRegistryAdapter } from "@exellix/narrix-scoper/adapters/nx-functions";

nx-rules adapter

import { createNxRulesRuleEngineAdapter } from "@exellix/narrix-scoper/adapters/nx-rules";

License

MIT