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

@centient/dag

v0.1.0

Published

Generic DAG scheduling core: adjacency, cycle detection, topological sort, parallel waves, and failure-cascade propagation

Readme

@centient/dag

Generic directed-acyclic-graph scheduling core. Pure computation, zero runtime dependencies. Operates over DAGNode<TId> — an id plus a dependsOn edge list — and makes no assumptions about node payloads: callers keep their domain data out of band (e.g. a Map<TId, Payload> keyed by node id).

Provides the primitives a scheduler needs:

  • Adjacency building — in/out edges + in-degree, with missing-dependency validation.
  • Cycle detection — DFS three-colour marking that returns the ordered cycle path.
  • Topological sort — Kahn's algorithm with deterministic tie-breaking.
  • Wave computation — parallelizable layers (longest-path level assignment).
  • Failure-cascade propagation — downstream nodes that become unreachable when a node fails.

Installation

npm install @centient/dag

Or as a workspace dependency in the monorepo:

pnpm add @centient/dag --workspace

Quick Start

import {
  topologicalSort,
  computeWaves,
  propagateFailure,
  detectCycle,
  type DAGNode,
} from "@centient/dag";

// TId narrows to your own string-literal union — fully type-checked.
type Stage = "build" | "test" | "lint" | "deploy";

const nodes: DAGNode<Stage>[] = [
  { id: "build", dependsOn: [] },
  { id: "test", dependsOn: ["build"] },
  { id: "lint", dependsOn: ["build"] },
  { id: "deploy", dependsOn: ["test", "lint"] },
];

topologicalSort(nodes);
// → ["build", "lint", "test", "deploy"]

computeWaves(nodes);
// → [["build"], ["lint", "test"], ["deploy"]]
//   each inner array can run concurrently

propagateFailure(nodes, "build");
// → { failed: ["build"], cascaded: ["deploy", "lint", "test"] }

Determinism

Every operation that emits node IDs is reproducible. Ties (independent nodes, peers within a wave) break with a single comparator — ascending Unicode codepoint order by default — so output never depends on Map/Set insertion order. Pass a custom IdComparator<TId> to any function to override the order. There is no Date.now() or Math.random() anywhere in the package.

const descending = (a: string, b: string) => (a < b ? 1 : a > b ? -1 : 0);
topologicalSort(nodes, descending);

API

Graph algorithms

| Function | Returns | Notes | |----------|---------|-------| | buildAdjacency(nodes) | AdjacencyGraph<TId> | in/out edges + in-degree; throws on unknown deps | | detectCycle(nodes, compare?) | CycleDetectionResult<TId> | { hasCycle, cycle }; cycle is an ordered closed path | | topologicalSort(nodes, compare?) | TId[] | Kahn's algorithm; throws DAGCycleError | | computeWaves(nodes, compare?) | TId[][] | parallelizable layers; throws DAGCycleError |

Failure cascade

| Function | Returns | Notes | |----------|---------|-------| | transitiveDependents(nodes, id, compare?) | TId[] | all downstream nodes (excludes id) | | propagateFailure(nodes, failed, compare?) | CascadeResult<TId> | { failed, cascaded }; accepts one or many failed ids |

Errors

  • DAGCycleError — thrown by topologicalSort / computeWaves on a cyclic graph. Carries cycle: ReadonlyArray<TId> (the ordered, closed path) and a human-readable message (DAG cycle detected: a -> b -> a).
  • DAGMissingNodeError — thrown when a dependsOn entry (or a cascade/query target) references a node not declared in the graph. Carries missingId and the referencing nodeId (when applicable). The core refuses to silently treat an undeclared dependency as a phantom node.

Wave semantics

computeWaves assigns each node to wave 1 + max(wave of its dependencies) — the longest-path distance from any root. Wave 0 holds dependency-free nodes. Every node inside a wave is mutually independent, so a scheduler may dispatch a whole wave concurrently and only advance to wave N+1 once wave N settles.

Failure cascade

When a node fails, everything that transitively depends on it can no longer run (its inputs will never be produced). propagateFailure returns that downstream set in cascaded so a scheduler can mark those nodes skipped/blocked instead of dispatching them. A node that is itself in the failed set is never double-counted as cascaded.

Provenance & divergences

This package was harvested from two copies of the same engine and cross-checked against the original:

  • Original: platform/crucible packages/crucible/src/orchestrator/dag.ts (+ its test suite).
  • Extracted copy: pipeline-sdk packages/orchestrator/src/dag.ts (header: "100% generic — extracted as-is from crucible").

Where the two disagreed, the original's behaviour was chosen unless the copy fixed a demonstrable bug. Recorded divergences:

  1. Cycle detection returns the cycle path, not a node set. The crucible original uses DFS three-colour marking and reconstructs the real ordered cycle (a -> b -> a). The pipeline-sdk copy reused Kahn's algorithm and returned only the unordered set of unvisited (cycle-participating) nodes, losing the path. We kept the original's path-returning behaviour — it is both the original and what the consuming spec requires ("the cycle path in the error").

  2. Missing dependencies are a hard error, not a silent phantom node. Both seeds tolerated a dependsOn id with no declared node (treating it as an implicit zero-edge node). This core throws DAGMissingNodeError instead, per the no-silent-degradation principle. Orchestrator-level schema validation (which previously caught this upstream) lives in the consumer, not here.

  3. Orchestrator-specific surface was deliberately dropped. DAGDefinition validation (kebab-case ids, chainMode, conflictStrategy, stageTimeoutMinutes), the DAGEngine class, ready-queue resolution, and chain extraction are pipeline/orchestrator concerns, not generic DAG scheduling — they stay in the consumer to preserve this package's zero-dep, payload-agnostic property. The failure classifiers (failure-classifier.ts in both seeds) classify failures by exit code / stderr and are likewise domain logic; this package owns failure cascade propagation through the graph, not failure classification.

License

MIT