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

@spekjs/core

v1.10.0

Published

Framework-agnostic engine for reading OpenSpec repositories — scanner, tasks parser, worktree aggregation and shared types.

Readme

@spekjs/core

Framework-agnostic engine for reading OpenSpec repositories.

It scans an OpenSpec directory, reads specs and changes, parses task checkboxes, and aggregates data across git worktrees (and, experimentally, jj workspaces) — with no framework dependencies. This is the engine behind spek, the OpenSpec viewer (Web, VS Code, IntelliJ).

Install

npm install @spekjs/core

Node.js only (ESM). Its sole runtime dependency is cross-spawn.

Usage

import { scanOpenSpec, readChange, parseTasks } from '@spekjs/core'

// Scan a repository's OpenSpec structure
const data = await scanOpenSpec('/path/to/repo')
// → { specs, activeChanges, archivedChanges, defaultSchema }

// Read one change, including its dynamically discovered artifacts
const change = await readChange('/path/to/repo', 'my-change-slug')
// → { slug, artifacts, schema, defaultSchema, schemaOrder, ... }

// Parse a tasks.md checkbox list
const tasks = parseTasks('- [x] 1.1 Done\n- [ ] 1.2 Pending\n')
// → { total: 2, completed: 1, sections: [...] }

Scanning never shells out to the OpenSpec CLI. readChange may query it once (cached) to resolve schema-authoritative artifact ordering, and degrades gracefully when the CLI is unavailable.

Main API

| Function | Purpose | | --- | --- | | scanOpenSpec(basePath) | Scan a single directory's OpenSpec structure | | scanOpenSpecAggregated(basePath, opts) | Scan across every worktree of the same repo | | readSpec(basePath, topic) | Read one spec, including its history | | readSpecAtChange(basePath, topic, slug) | Read a spec as of a given change | | readChange(basePath, slug, orderProvider?) | Read one change and its artifacts | | parseTasks(content) | Parse - [x] / - [ ] checkboxes, grouped by ## section | | buildGraphData(basePath) | Build spec–change relationship graph data | | buildGraphDataAggregated(basePath, opts) | The same, aggregated across worktrees | | listWorktrees(basePath) | List every git worktree of the same repo | | listWorkspaces(basePath, opts?) | The same, plus jj workspaces when includeJj is set | | shouldUsePolling(path, opts?) | Decide whether file watching must fall back to polling |

jj (Jujutsu) workspaces — experimental

scanOpenSpecAggregated and buildGraphDataAggregated accept includeJj (default false). When enabled, active changes are also collected from every jj workspace of the repo.

const data = await scanOpenSpecAggregated('/path/to/repo', { aggregate: true, includeJj: true })

jj copies do not go through the git-divergence election used for git worktrees — a jj working-copy commit is a change id, not a git ref, and every workspace materialises the full trunk. They are deduplicated by content fingerprint instead: identical copies collapse to one, a diverged copy keeps its own entry flagged conflictsWith, and the copy that @ is editing is flagged isCurrent.

Nothing is spawned unless jj is requested, and jj is never required: with the CLI absent or the directory not a jj repo, the jj helpers resolve to [] and results are identical to includeJj: false.

Subpath exports

import { extractHeadings, slugifyHeading, specHeadingLabel } from '@spekjs/core/headings'
import {
  DEFAULT_ORDER,
  defaultRank,
  sortArtifacts,
  ARTIFACT_SORT_MODES,
} from '@spekjs/core/artifact-order'

These subpaths are kept separate so browser bundles can import the utilities without pulling in server-only modules.

specHeadingLabel(text) returns what a host should display for a spec heading: the leading OpenSpec format keyword (Requirement: / Scenario:) removed, anything else returned unchanged. Pass the whole heading text. It is display-only — Heading.text and Heading.slug stay as authored, and nothing is derived from the label, least of all a slug: slugs are the anchors every link resolves against.

sortArtifacts(artifacts, mode, schemaOrder?) orders a change's artifacts by one of ARTIFACT_SORT_MODESmodified (the order given), schema (the authoritative sequence, falling back to the narrative order when unavailable), or alpha (by title). ArtifactSortMode is derived from ARTIFACT_SORT_MODES, so validating a persisted preference against the array cannot drift from the type. Callers must not mutate a returned list: under modified it may be the array they passed in.

It is generic in the element type, so your own artifact type comes back out — no cast, and fields of your own stay reachable on the result:

interface MyArtifact extends ChangeArtifact {
  relPath: string
}

const sorted: MyArtifact[] = sortArtifacts(myArtifacts, 'schema', order)
sorted[0].relPath // still typed — no cast anywhere

The element only has to carry the two fields the rule reads — id (the narrative rank, the schemaOrder lookup and both tiebreaks) and title (alpha) — so it need not be a ChangeArtifact. One missing either is rejected at compile time.

License

MIT © Kewang