@takk/bayescausal - v1.0.0 - Apache-2.0

Find the root cause while the incident is still open.

A log search tells you which symptom fired; it does not tell you why. BayesCausal lets you declare the causal graph of your system once, observe the symptoms of an incident as evidence, and get a ranked list of probable root causes with calibrated posterior probabilities.

125tests passing
97%coverage
0runtime deps
20subpaths
What it is

One library, two answers.

In a few words

BayesCausal models your system as a causal graph: components that can fail, the symptoms they produce, and how one depends on another. When an incident fires, you tell it which symptoms you observed, and it returns a ranked list of probable root causes with calibrated posterior probabilities, the single most probable explanation, and a human-readable report. Your code stays small: you call network.observe(node, state) for each symptom, then read network.diagnose().

Technically

The graph is a directed acyclic graph of discrete variables, each carrying a conditional probability table. Observing a symptom conditions the joint distribution; exact inference by variable elimination and junction-tree belief propagation returns the exact posterior, and loopy belief propagation, likelihood weighting, and Gibbs sampling scale to larger graphs with convergence and credible intervals. The do-operator answers interventional queries P(Y | do(X)), most-probable-explanation returns the joint assignment, and online Dirichlet learning, an explanation generator, and a tamper-evident hash-chained audit trail round it out.

Before and after

The same incident, two very different on-call shifts.

Take a real production scene: latency spikes across a service, several alerts fire at once, and you need the root cause before the page escalates, not a wall of symptoms to read by hand.

With log search and intuition

Symptoms everywhere, the cause inferred by hand

  1. Minute 1 A dozen alerts fire at once; every one is a symptom, none names a cause.
  2. Minute 5 You grep dashboards, correlating spikes by eye under time pressure.
  3. Minute 10 Two plausible causes look equally guilty and nothing ranks them.
  4. Minute 20 A confounded metric points at the wrong component, and you chase it.
  5. Minute 30 The on-call escalates because the suspect list never narrowed.
  6. Minute 40 The real cause surfaces by luck, not by a calibrated probability.
  7. Postmortem Nobody can reconstruct why each hypothesis was ruled in or out.
With BayesCausal

Calibrated diagnosis, the cause ranks itself

  1. Second 1 Each observed symptom folds into the network as evidence over the graph.
  2. Second 1 Belief propagation conditions the joint distribution on what you saw.
  3. Second 2 The ranked diagnosis returns probable root causes with calibrated posteriors.
  4. Second 2 The most probable explanation names the single joint assignment that fits best.
  5. Second 3 A do-operator query separates a confounded correlation from a true cause.
  6. Decision The explanation generator writes a human-readable report you can paste into the channel.
  7. The audit log records every diagnosis, sealed and verifiable, for the postmortem review.

The "With BayesCausal" timeline is the behavior of exact inference on a declared graph; the causal graph is declared by you, not discovered, and the diagnosis is a ranked posterior over the causes you modeled, not an automated remediation.

Install

Five minutes from install to your first calibrated diagnosis.

1. Add the package

pnpm add @takk/bayescausal
npm install @takk/bayescausal
yarn add @takk/bayescausal
bun add @takk/bayescausal

2. Zero required dependencies

The core has no runtime dependencies and a node-free core. Every @takk sibling is an optional peer; install only what you compose with.

# nothing else is required to start diagnosing
              # optional: install a sibling you compose with, for example
              pnpm add @takk/keymesh @takk/modelchain

3. Diagnose an incident in a few lines

import { createNetwork } from '@takk/bayescausal';

              const network = createNetwork({ nodes: [
              { id: 'ProviderDegraded', states: ['yes','no'], role: 'cause', faultStates: ['yes'], cpt: [[0.1,0.9]] },
              { id: 'TrafficSpike', states: ['yes','no'], role: 'cause', faultStates: ['yes'], cpt: [[0.15,0.85]] },
              { id: 'Latency', states: ['high','normal'], role: 'symptom', parents: ['ProviderDegraded','TrafficSpike'],
              cpt: [[0.95,0.05],[0.7,0.3],[0.6,0.4],[0.05,0.95]] },
              ]});

              network.observe('Latency', 'high');
              console.log(network.diagnose().ranked); // ranked root causes with calibrated posteriors

4. Wire it to the observability stack you already use

import { createNetwork } from '@takk/bayescausal';
              import { createObserver } from '@takk/bayescausal/observability';

              const network = createNetwork({ template: 'serving' });

              // A dependency-free adapter: emit one span per diagnosis and a metric per
              // posterior. Bind to OpenTelemetry, a raw collector, or a non-human-entity loop.
              const observer = createObserver(network);
              network.observe('Latency', 'high');
              const { ranked, mostProbableExplanation } = observer.diagnose();
              // ...the observer emits the span and metrics, then:
              console.log(ranked); // ranked root causes with calibrated posteriors
Features

Nine capabilities, every one tied to a measurable outcome.

Exact inference

Variable elimination and junction-tree belief propagation return the exact posterior over every node, so a diagnosis on a declared graph is a true conditional probability, not a sample estimate.

You get the exact answer the model implies, reproducible to the last digit, when the graph is small enough to solve exactly.

Approximate inference

Loopy belief propagation, likelihood weighting, and Gibbs sampling scale to larger graphs, and each reports convergence and a credible interval so you read how settled the estimate is.

You keep diagnosing on a graph too large for exact inference, with an honest measure of how far the approximation has converged.

The do-operator

Interventional queries answer P(Y | do(X)), which separates seeing from doing. A confounded observational effect of 0.380 becomes an interventional 0.200 once the back-door is cut.

You tell a true cause from a confounded correlation, instead of acting on a number that only looks causal.

Most probable explanation

Beyond a per-node posterior, MPE returns the single joint assignment of all causes that best explains the observed symptoms, the one coherent story rather than a marginal per node.

You get one explanation that fits every symptom at once, instead of stitching independent marginals into a guess.

Online parameter learning

Conjugate Dirichlet updates fold each labeled incident into the conditional probability tables online, so the network sharpens from real outcomes without a full retrain.

Your graph improves from every postmortem you label, instead of staying frozen at the numbers you guessed on day one.

Diagnosis ranking and report

Diagnosis returns a ranked list of probable root causes with calibrated posteriors, and the explanation generator writes a human-readable report of why each one ranked where it did.

A human or a non-human entity reads a ranked, explained diagnosis it can paste into the channel, rather than a wall of raw probabilities.

Tamper-evident audit

An append-only log of every observation and diagnosis, with a SHA-256 hash-chain you append to and verify via the Web Crypto API.

Prove a diagnosis was made on the recorded evidence, in Node, edge runtimes, or the browser, when a postmortem conclusion is questioned.

Prebuilt incident templates

Start from a prebuilt incident graph template instead of a blank canvas, then snapshot the network to portable JSON and restore it on the next run.

You diagnose a common topology in minutes from a template, and your declared graph and learned tables survive restarts and cold starts.

SLSA provenance

Every published version signed with npm publish --provenance through GitHub Actions OIDC. Lockfile committed, zero required runtime dependencies, node-free core.

Verify in one command that the tarball you installed was built from the source commit you trust.

Priors

Explicit priors, never a black box.

Pass a prior over the conditional probability tables globally, or override it per node. The graph stays yours; the inference is the library's.

Prior What it encodes When to use it
UNIFORM_PRIOR Dirichlet(1, ..., 1): every conditional distribution equally likely before any data. Default. No prior knowledge about a node's table yet.
JEFFREYS_PRIOR Dirichlet(0.5, ..., 0.5): the reference objective prior for a categorical. When you want the standard objective prior with minimal influence.
EXPERT_PRIOR Pseudo-counts that encode an engineer's belief about a fault before any incidents. A known failure mode you can quantify should anchor the table until data accrues.
perNodePrior A per-node override map, so each component starts from its own profile. A well-understood dependency and a freshly modeled one should not share one belief.

Any Dirichlet prior is accepted; pass your own pseudo-counts globally or per node. Online learning folds each labeled incident into the conditional probability tables with a conjugate update.

Entry points

Twenty subpaths, import only what you need.

Entry point Subpath export Runtime Use it when
Facade @takk/bayescausal node-free You want the createNetwork facade and the full toolkit.
Observability @takk/bayescausal/observability node-free You want the dependency-free adapter that emits a span and metrics per diagnosis.
Inference engines @takk/bayescausal/inference node-free You want variable elimination, junction tree, or an approximate engine on its own.
Node store @takk/bayescausal/node Node only You need durable, file-backed persistence that survives restarts.
CLI

A command-line tool that runs the real inference.

The bayescausal binary runs the compiled engine, so every number it prints comes from execution. demo walks a prebuilt incident graph end to end; diagnose folds observed symptoms and prints the ranked root causes; evaluate scores diagnosis accuracy on a template.

Walk a prebuilt incident graph

# declare a template graph, observe a symptom, print the ranked diagnosis
              npx @takk/bayescausal demo \
              --template serving

Diagnose from observed symptoms

# node=state observations, from flags or stdin
              echo "Latency=high ErrorRate=high" | npx @takk/bayescausal diagnose --template serving
          

Evaluate, version, and help

npx @takk/bayescausal evaluate --template serving
              npx @takk/bayescausal --version
              npx @takk/bayescausal help

              # exit codes: 0 ok, 64 usage, 65 data error, 66 missing input
Audit and explanation

Prove what happened, and read the reason it happened.

Record every observation and diagnosis in a tamper-evident, hash-chained log. And because the diagnosis is explicit, diagnose returns the ranked causes, their calibrated posteriors, and the most probable explanation, so a conclusion, or a wait for more evidence, is never a black box. Both are node-free and need no external service.

import { AuditLog } from '@takk/bayescausal/audit';

              const log = new AuditLog();

              await log.append({
              observed: { Latency: 'high' },
              topCause: 'ProviderDegraded', posterior: 0.82,
              decided: false, reason: 'awaiting-evidence', at: Date.now(),
              });

              await log.verify(); // true, until any entry is altered

Every diagnosis is explicit

const diagnosis = network.diagnose();
              // {
              //   ranked: [               // probable root causes, highest posterior first
              //     { cause: 'ProviderDegraded', posterior: 0.78 },
              //     { cause: 'TrafficSpike',     posterior: 0.19 },
              //   ],
              //   mostProbableExplanation: { ProviderDegraded: 'yes', TrafficSpike: 'no' },
              //   method: 'variable-elimination'
              // }

The method is one of variable-elimination, junction-tree, loopy-belief-propagation, likelihood-weighting, or gibbs, so you can log and alert on exactly which engine produced a diagnosis and how it converged. Wire the observer hook to emit an OpenTelemetry span per diagnosis and a metric per posterior, with no runtime dependency, the governance seam for a non-human entity's incident triage.

Compare

BayesCausal vs the alternatives.

The other approaches solve part of the problem. The contrast clarifies where BayesCausal sits.

Capability BayesCausal Log search Correlation alerts Rules-engine SaaS Hand-rolled
Distribution npm library query console hosted service hosted service your repo
Ranked root causes calibrated posteriors symptoms only correlation varies rarely
Most probable explanation joint MPE manual no partial no
Interventional do-operator yes no correlation only some no
Exact and approximate engines both n/a no varies rarely
Tamper-evident audit yes no no partial no
Zero deps, node-free yes n/a no no varies
License Apache-2.0 proprietary proprietary proprietary your call

The honest summary: a log search or a correlation alert is fine when the cause is obvious and one dashboard tells the whole story. Pick BayesCausal when symptoms fan out across components, you want ranked root causes with calibrated posteriors, the most probable explanation, an interventional do-operator, and a tamper-evident trail composed in one node-free TypeScript library you embed in production.

The model, honestly

What the model assumes, and how it stays honest.

BayesCausal is Bayesian inference over a declared graph, and that has assumptions. They are documented, and where a real incident strains them, there is an explicit mitigation rather than a silent fudge.

Assumption or property What it means How BayesCausal handles it
Graph declared, not discovered You author the directed acyclic graph and its tables; the library does not learn structure from data. Prebuilt incident templates give a vetted starting graph, and online learning refines the tables.
Exact is exact Variable elimination and the junction tree return the true posterior the graph implies. The engine is selected automatically, and exact is used whenever the graph is small enough to solve.
Approximation reports itself Loopy belief propagation and sampling estimate the posterior rather than compute it. Each approximate engine reports convergence and a credible interval, so you read how settled it is.
Discrete states Each node takes one of a fixed set of categorical states. Continuous signals are bucketed into states you name, with the buckets documented in the graph.
Calibrated posterior A ranked cause's probability is only as calibrated as the tables you supplied. Diagnosis evaluation reports top-1 accuracy and a Brier score so you can measure calibration.
Interventional, not counterfactual The do-operator answers P(Y | do(X)), the effect of acting, not a what-if on a past case. Interventions ship now; counterfactual queries are on the roadmap and the docs say so plainly.
Seeing versus doing An observational correlation can be confounded and overstate a cause. The do-operator cuts the back-door, turning a confounded 0.380 into an interventional 0.200.
Integrity, not identity The audit seal proves a log was not altered after sealing. A SHA-256 hash chain via Web Crypto, an integrity seal, not a signature.
Quality and validation

The receipts behind v1.0.0.

Tests & coverage

125 tests passing under Vitest 4 with 97% coverage. Diagnosis evaluation on the serving template reaches 78% top-1 accuracy with a Brier score of 0.0409. Run pnpm test on a fresh clone to reproduce.

Type safety

TypeScript 6 in maximum strict mode (exactOptionalPropertyTypes, useUnknownInCatchVariables, noUncheckedIndexedAccess, noImplicitOverride, noImplicitReturns). Zero errors under tsc --noEmit.

Lint & types-correctness

Biome 2.5 clean across src and tests. publint clean and @arethetypeswrong/cli green across all twenty subpaths. Dual ESM + CJS with separate .d.ts and .d.cts per subpath.

Exact math, verified

Exact inference is checked against hand-computed posteriors, and the do-operator separates seeing from doing: the observational effect of 0.380 becomes an interventional 0.200 on the confounded example. The credible intervals are the engine's own, not a hand-wave.

Distribution smoke

A smoke test exercises the compiled ESM and CJS artifacts and spawns the compiled CLI as a single Node process, run on the Node 20, 22, and 24 matrix.

Supply chain

Committed pnpm lockfile, zero required runtime dependencies, node-free core, and SLSA provenance attestation on every published version. Verify with npm view @takk/bayescausal@1.0.0 --json | jq .dist.attestations.

Roadmap

What is shipped, what is next, what is later.

Now (1.0)

Shipped in v1.0.0

  • Exact inference: variable elimination and junction tree
  • Approximate: loopy BP, likelihood weighting, Gibbs
  • The do-operator for interventional causal effects
  • Most-probable-explanation, online Dirichlet learning
  • Prebuilt incident templates, diagnosis ranking
  • Explanation generator, observability adapter, CLI
  • Dual ESM + CJS, node-free core, SLSA provenance
Next

Planned next

  • Counterfactual queries on top of the do-operator
  • SQLite, Postgres, and Redis store backends
  • First-class OpenTelemetry and collector bindings
  • Signed and timestamped audit seals
  • More prebuilt incident graph templates
Later

On the horizon

  • Continuous and hybrid node distributions
  • Structure-learning assists for graph authoring
  • Federated graph sharing across organizations
  • Hosted dashboards for incident diagnosis
FAQ

Common questions.

Is BayesCausal production-ready at 1.0.0?

Yes. 125 tests pass under Vitest 4 with 97% coverage; TypeScript 6 maximum strict mode is clean; Biome 2.5 lint is clean; publint and @arethetypeswrong/cli are clean across all twenty subpaths. The suite and the distribution smoke test are run on the Node 20, 22, and 24 matrix. Every published release carries SLSA provenance produced by GitHub Actions.

How is this different from a log search or an alert rule?

A log search or an alert rule tells you a symptom fired; it does not tell you why. BayesCausal models the causal graph between components and symptoms, so when you observe the symptoms of an incident as evidence it returns a ranked list of probable root causes with calibrated posterior probabilities and the most probable explanation. On the serving template that reaches 78% top-1 accuracy with a Brier score of 0.0409.

Does the do-operator handle counterfactuals?

The do-operator is interventional: it answers P(Y | do(X)), which separates seeing from doing, so a confounded observational effect of 0.380 becomes an interventional 0.200 once the back-door is cut. Counterfactual queries are on the roadmap. The causal graph is declared by you, not discovered from data, and the docs are explicit about both points.

What is the difference between exact and approximate inference?

Exact inference by variable elimination and junction-tree belief propagation returns the exact posterior the graph implies. The approximate engines, loopy belief propagation, likelihood weighting, and Gibbs sampling, scale to larger graphs and each reports convergence and a credible interval, so you can check the diagnosis as often as you like and know how settled the estimate is. The docs are explicit about which guarantee each one gives.

Does this work in Cloudflare Workers, Vercel Edge, Bun, or Deno?

Yes. The core is node-free; the audit seal uses the Web Crypto API, not node:crypto. Import @takk/bayescausal on any runtime with Web Crypto, and wire the observability adapter to OpenTelemetry, a raw collector, or a non-human-entity loop. Only @takk/bayescausal/node requires the Node standard library.

How does BayesCausal handle my data?

BayesCausal records the node identifiers and states you choose, the conditional probability tables, the observed symptoms, and timestamps. It never sees your raw logs or payloads and makes no outbound network calls of its own. With the file store, only that network state reaches disk; no secrets are involved at any point.

What happens before any incident is labeled (cold start)?

A node starts from its prior, the uniform Dirichlet by default, so the table reflects only what you declared until data accrues. To start from a better belief, pass an EXPERT_PRIOR or a perNodePrior override, then let online learning fold each labeled incident into the tables with a conjugate update.

Where does the state live?

In-process memory by default, with portable JSON snapshots via snapshot() and load(). For durability across restarts, use createFileStore from @takk/bayescausal/node. For an edge runtime, snapshot to your own KV store between invocations.

Is the audit trail a digital signature?

No, and the docs are explicit about it. The seal is a SHA-256 hash-chain root that proves a log was not altered after sealing. It is an integrity seal, not a signature: it does not prove who produced the log. Signed and timestamped seals for stronger third-party evidence are on the roadmap.

What kinds of nodes can the graph hold?

Each node is a discrete variable with a fixed set of categorical states and a conditional probability table over its parents. Mark a node as a cause or a symptom and name its fault states, and a continuous signal is bucketed into states you define. Continuous and hybrid distributions are on the roadmap.

How do I verify a published version's provenance?

Every release is published with npm publish --provenance. Check the attestations with npm view @takk/bayescausal@<version> --json | jq .dist.attestations. The attestation links the tarball you installed to the GitHub Actions workflow that built it from a specific source commit.

Can I plug in my own store backend?

Yes. Snapshot the network to portable JSON with snapshot() and restore it with load(), persisting that JSON wherever you like, SQLite, Postgres, Redis, or a KV store. The createFileStore file store is the zero-dependency reference for Node.

What is the policy on breaking changes?

Strict SemVer 2.0.0, starting from 1.0.0. The binding stability surface is documented in SPEC.md section 5. Major bumps require a deprecation cycle; security fixes follow the disclosure flow in SECURITY.md.

Author

Built and maintained by David C Cavalcante.

David C Cavalcante

Founder, Takk Innovate Studio

Product Engineer, ML Engineer, LLM Engineer, LLM Architect, Massive Intelligence (IM) Researcher. Builder of the @takk family of NPM packages for Massive Intelligence (IM) native infrastructure.

BayesCausal is part of a planned portfolio of NPM libraries targeting Massive Intelligence (IM) native infrastructure for 2026 to 2030. Adjacent research by the author covers systemic intelligence frameworks (MAIC, HIM, NHE) published independently of this codebase, with research notes on PhilPapers and PhilArchive linked from the repository README.

If BayesCausal named your root cause this quarter, the most useful thing you can do is open a GitHub issue when you find an edge case the test suite missed. The runbook for releases, the threat model, and the contributor agreement all live in the repository.