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

@issuegraph/derive

v0.2.0

Published

Derive the selection order from an Issuegraph model — ranked slots, held slots with their reasons, together units, excluded duplicates, priority promotions — plus the pre-write cycle refusal. Pure; no network, no tracker, no writes.

Readme

@issuegraph/derive

Turn an Issuegraph model into the order you should work in — and refuse a blocked-by edge that would make that order impossible.

One layer above @issuegraph/reader. The reader answers what the graph is — ready set, effective priority, serialize and together components, duplicates, cycles. This answers what order follows from it, with every row carrying the reasons it sits where it does.

npm install @issuegraph/derive

The order

import { deriveIssueOrder } from '@issuegraph/derive';

const derived = deriveIssueOrder({
  issues,                                   // the same NodeInput[] buildModel takes
  config: { baseRanking: { source: 'config', order: myRankedRows } },
});

derived.slots;        // every position, in order
derived.rankOf;       // key -> rank, or null when this order cannot place it
derived.priority;     // key -> declared/effective/promoted-by, in the spec's notation
derived.excluded;     // duplicates, and the canonical each defers to
derived.provenance;   // decomposed-from edges, which order nothing
derived.diagnostics;  // anomalies worth surfacing to a groomer
derived.wouldCycle;   // bound to this node set, so a refusal costs no round-trip

Your ranking is the input, not something this computes. Whatever already orders your backlog — mapped labels, issue types, saved queries, a tie-break of your own — is supplied through baseRanking and never re-derived here. Hosts routinely evaluate that in a database; a second engine beside it would be a mirror whose input space drifts.

Frontmatter modifies that ranking; it never replaces it. One sort with a swappable secondary key:

(effectivePriority ASC, baseRankingPosition ASC, issueNumber ASC)

At zero adoption every effective priority equals its declared priority, so the primary key collapses to the band your ranking already produced and the output is your ranking. Getting this backwards makes the package useless to anyone who has not adopted the format — which is everyone at first. Frontmatter only ever moves an issue between bands, out of the order, or into a held slot.

What a slot is

A together unit occupies one slot, not one per member (§4.3.7) — the group is one piece of work, and it advances only when every member can.

A held slot keeps its position, and whether it keeps a number depends on where the thing it waits on is:

  • Blocker inside this order — every hold names an issue that has a row here — the slot keeps its rank. #512 ⊘ blocked-by #488 sits at rank 2 while #488 sits at rank 1, because the reader can follow the hold to a row in front of them.
  • Blocker outside it — not in the node set, unresolvable, or a hold that names no issue at all — the slot carries rank: null and a wouldBeRank: the position it would take, which is one past the last rank issued. It consumes nothing, so the next ranked slot takes that number instead.

ready is the only field that answers may this start; a rank answers where does it sit. holdReasons names each failed condition, and holds carries the same conditions as the reader's { code, subject?, text }holdReasons is its text projection — so a host groups on code and links subject without matching a sentence.

Changed in 0.2.0. Before it, every held slot carried rank: null.

  • A consumer that read rank !== null as ready must read ready instead. That inference was sound and is not any more.
  • Every rank below a held-but-in-order slot shifts by one, because such a slot now consumes a number. Only the held-outside arm leaves the numbering alone. [A held-inside, B held-outside, C ready, D ready] gave [null, null, 1, 2] and now gives [1, null, 2, 3].

promotedBy names the neighbour the urgency arrived through — along both paths §6.3 relaxes, blocked-by and together-with, since a P3 grouped with a P0 is genuinely promoted and a blocked-by-only index would report that with nothing to show for it. The together half is the adjacent peer, not the component: relaxation puts every member at the same effective priority, so enumerating the component makes a stranger three hops away read as a cause. It reads exactly the edges the model read — an edge naming a duplicate is attributed to its canonical, and a duplicate's own edges are ignored. Over-refusing is safe for a pre-write guard and wrong for provenance: it would name a cause that did not act.

Group sizes are computed, never read. No issue writes its group down (§4.3.4, §4.3.7). Both sizes count live candidates only, so a serialize partner that shipped stops counting, exactly as a closed together member does.

The cycle refusal

import { wouldCycleOnBlockedBy } from '@issuegraph/derive';

wouldCycleOnBlockedBy(issues, from, to, { homeRepo });   // true = refuse the write

Synchronous by signature — no client, no handle, no promise — which is what makes zero round-trips a property of the contract rather than of an optimization. An edge that closes a cycle produces a component no member of which can ever be ready (§6.6), and once written nobody can unstick it.

Duplicates resolve the model's way. §4.3.3 makes an edge naming a duplicate name its canonical, and the walk follows the same resolution — through Model.duplicateCanonical, not a second duplicate-chain walk. Skip it and the guard fails open: with #30 duplicating #10, the model reads "#20 blocked-by #30" as "#20 blocked-by #10", so #10 blocked-by #20 closes a cycle a raw walk never finds.

That applies to the two arguments as well as to the stored edges, and each is walked under both spellings — the key as given and its canonical. Resolving only the stored edges leaves from and to in a different key space from the edges they are compared against, so #20 blocked-by #30 where #30 duplicates #20 is a self-dependency the probe never sees. Replacing the raw spelling instead of adding to it would remove refusals, because a duplicate still has outgoing edges under its own key (see the third divergence). Both is monotone: it can only refuse more.

Three deliberate divergences from model.cycles, all fail-safe:

  • It walks closed nodes too. model.cycles filters to open ones, because a closed blocker does not block today. This is a question about the future: the edge outlives the current states, and a reopened issue makes the cycle real.
  • A target outside the supplied set answers false, not a refusal. A documented precondition: both endpoints must be present for the answer to mean anything. Failing closed would make a paged editor refuse every edge to an issue it has not loaded.
  • An edge declared by a duplicate is kept. buildModel drops a duplicate's own edges; matching it here is the one place copying the model would make this guard weaker. Note the asymmetry: resolving a target adds reachability and is adopted, filtering a declarer removes it and is not. A groomer who clears the duplicate-of brings the edge back — with the cycle already written.

Where it plugs in

The two entry points are exactly the two ports @issuegraph/store declares and deliberately does not implement — OrderDeriver and EdgeGuard. The store owns when the order is recomputed; this owns what it is.

Purity

No fetching, no mutation, no persistence, no clock, and nothing stored between calls. That is why two clients reading the same issue bodies derive the same order without coordinating, and why the result cannot go stale.

It is pinned mechanically rather than asserted: the tests read the modules' own syntax tree for a dependency outside a four-entry allowlist and for module-scope mutable state, and pin freshness and reference identity so a memo returning the stored model fails — which a compare-by-value test alone would let through, since returning the identical object satisfies every deep-equal.

DerivedIssueOrder is an in-process value, not a payload. wouldCycle is a live closure and two fields are Maps, so JSON.stringify silently drops the function and flattens the maps to {}. Project it explicitly at any serialization boundary rather than handing it over whole.

Versioning

0.x, and unstable — it tracks a draft specification, so a minor bump may break you. Pin exactly if that matters.


Apache-2.0 · stewarded by Autonomy LLC.