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

@miadi/ncp-story-studio

v0.8.0

Published

Narrative Context Protocol story studio as consumable modules: canonical NCP types and a zero-dependency validator, a headless story-lattice layout with a React renderer, story-project storage providers (JSON files and Neon Postgres), the agent link proto

Readme

@miadi/ncp-story-studio

The Narrative Context Protocol story studio, as consumable modules: canonical NCP types and a zero-dependency validator, a reader that accepts the NCP dialects that actually exist, a headless story-lattice layout with a React renderer, story-project storage over JSON files or Neon Postgres, the agent link protocol, and the canonical terminology as data.

Phase plan, sources of truth, and what is deliberately not built yet: PHASES.md.

Install

pnpm add @miadi/ncp-story-studio
# in this workspace
pnpm -w add @miadi/ncp-story-studio@workspace:* --filter <consumer>

react and @neondatabase/serverless are optional peer dependencies: they are needed only by /graph/react and /store/neon.

Modules

| Subpath | Needs | What it is | |---|---|---| | @miadi/ncp-story-studio | — | Package identity plus the schema surface | | .../schema | — | Types, enums, validator, dialect reader, completeness | | .../graph | — | Story lattice geometry, tone palette, SVG output | | .../graph/react | react | <StoryGraph> — a client component | | .../store | — | Story projects, pure edits, the provider contract | | .../store/json | node | JSON files, one folder per project | | .../store/neon | node, @neondatabase/serverless | Postgres, one row per project | | .../agents | — | Personas, lattice context, the [[Beat:id\|label]] link protocol | | .../terminology | — | The canonical NCP vocabulary as data | | .../fixtures | node | Nine bundled stories: six from the studio's own prototype, three canonical NCP examples |

Schema

import { readNcp, validateNcpDocument, storyCompleteness } from '@miadi/ncp-story-studio/schema'

const { document, dialect, notes } = readNcp(JSON.parse(raw))
const result = validateNcpDocument(document)

result.valid          // no issue of severity `error`
result.issues         // { path, code, severity, message } — evidence, not a score
result.extensions_used// which declared extensions this document relies on
storyCompleteness(document.story).overall // 0..1, measured against named targets

Three dialects, one reader

NCP files in circulation are not all shaped the same way, and none of the differences is a defect — they are what different tools wrote. readNcp recognizes each and reports every adaptation it makes.

| Dialect | Shape | Example | |---|---|---| | canonical | { schema_version, story { narratives[…] } } | example-story.json | | subtxt-export | narratives beside story, perspectives keyed by point of view, context strings, Dramatica display labels, placeholder ids | the-shawshank-redemption.json, anora.json | | miadi-design | NCP container names with a design vocabulary (label, description, event, structural_function) | weaver_of_words__the_catalyst_of_change_251101.json |

Display labels ("Story Goal", "Success", "Main Character Resolve") are mapped to the canonical identifiers through the protocol's own terminology data; whatever cannot be mapped is declared in a custom_* field rather than silently dropped.

Declared extensions

Fields this workspace adds beyond the canonical schema are declared, not drifted: perspective.throughline, player.motivations, storypoint.custom_appreciation, storypoint.custom_narrative_function, storybeat.custom_narrative_function, dynamic.custom_dynamic, dynamic.custom_vector. validateNcpDocument reports each one in use, and { allowExtensions: false } turns their use into an error.

Two strictness levels

An issue that is about particular elements names them in elementIds — two beats sharing a sequence are both named, though the issue's path points at the collection — so a surface can take the reader to them.

By default the validator checks what a studio needs: identity, referential integrity, enum membership, and per-series sequence uniqueness. { requireCanonicalFields: true } adds the canonical schema's own required lists — which name every field, including visual, audio, illustration and tones. None of the shipped canonical examples satisfies that, which is why it is opt-in.

Graph

import { layoutNarrative, layoutToSvg } from '@miadi/ncp-story-studio/graph'
const layout = layoutNarrative(narrative, { beatSpacing: 320 })
const svg = layoutToSvg(layout)          // server-rendered, no DOM
import { StoryGraph } from '@miadi/ncp-story-studio/graph/react'
<StoryGraph narrative={narrative} focusedBeatId={id} onBeatClick={setId} />

The line is navigable: drag the empty space to pan, arrow keys scroll it (shift for a longer step, Home and End for the ends), and a press that starts on a beat still opens that beat. cards={false} gives a compact strip — still a caption per beat, with its narrative function, its summary and its three tone letters, so a long story stays readable at a glance.

On a phone

import { BeatScrubber, StoryGraph } from '@miadi/ncp-story-studio/graph/react'

<BeatScrubber narrative={narrative} currentBeatId={position} onScrub={goToBeat} />
<StoryGraph
  narrative={narrative}
  focusedBeatId={focused}
  snap={isTouch}                    // one beat comes to rest at the centre, like a carousel
  onCenteredBeatChange={setPosition} // the beat a swipe settled on
/>
  • The graph follows its focus. When focusedBeatId changes the line scrolls that beat to the centre (followFocus={false} turns it off), so a link to beat 40 lands on beat 40.
  • snap makes a swipe settle with one beat centred. A mouse drag suspends it while held.
  • onCenteredBeatChange reports the beat a reader's own scroll settled on. The scroll the graph makes itself to follow a focus is not reported, so a host can feed it straight back into focusedBeatId without the two chasing each other.
  • A vertical swipe on the line scrolls the page. Touch is left to the browser's native pan; only a mouse drag is captured.
  • centerBeatId keeps a beat at the centre apart from the focus ring — pass where the reader is (a scrubber position, a list scrolled by hand) and keep focusedBeatId for the beat they chose. The line is put back on that beat when its layout changes (cards ↔ compact).
  • A tap on a card opens its beat, as the ring always did; a mouse drag that starts on a card still pans.
  • <BeatScrubber> is one tick per beat, coloured by its abstraction tone and sized by its appreciation (signposts tallest). A thumb runs along it; onScrub fires at most once a frame as beats are crossed, and once more with the beat the thumb lifted on, then onScrubEnd. Holding the thumb still for 300ms switches to fine mode, 12px a beat. The counter under it takes a number. It is a role="slider" with a visible focus ring — arrow keys step, Page Up/Down step ten, Home and End go to the ends.
  • Only the beats around the view are drawn — a dozen, however long the story — so a phone holds a ninety-beat line without running out of memory. The canvas keeps its full width.
  • The pure geometry behind both is exported from /graph for any other surface: nearestNodeIndex(layout, x), scrollLeftForNode(layout, index, viewportWidth), scrubIndex(count, fraction), visibleNodeRange(layout, scrollLeft, viewportWidth, pad).

Cards clamp their summary and illustration to three lines, so the canvas is 480px tall rather than 720 — the full telling is one tap away, and a phone shows the line and its tones on one screen.

Store

import { JsonStoryStore } from '@miadi/ncp-story-studio/store/json'
import { NeonStoryStore } from '@miadi/ncp-story-studio/store/neon'

const store = new JsonStoryStore({ root: '/srv/miadi/ncp-story-projects' })
await store.init()
const project = await store.get('catalyst-of-change')
await store.save({ ...project, document }, { expected_updated_at: project.updated_at })

expected_updated_at makes a write conditional. On Neon the guard is part of the statement, so two concurrent writers cannot both succeed. On the filesystem it is enforced within one process and is best-effort across processes — a filesystem has no atomic compare-and-set.

The JSON provider keeps the document as a plain NCP file (story.ncp.json) beside its metadata (project.json), so exporting a project is copying it.

Story edits (updateStorybeat, reorderStorybeats, upsertPlayer, …) are pure: they return a new story and never mutate the one they are given, so undo is keeping the previous value.

Agents

import { runAgentTurn, parseAgentMessage } from '@miadi/ncp-story-studio/agents'
const response = await runAgentTurn(model, { story, message })
response.segments  // text and links, ready to render
response.links     // every link with whether it resolves in this story
response.provenance// which model answered, and how the story was carried

AgentModel is an interface with one method, so the package depends on no vendor SDK. Personas: 🧠 Mia reads structure, 🌸 Miette reads meaning, 🎨 Ava8 reads tone and musical shape.

The registry is open. A story can bring reading positions of its own — pass them as extraPersonas, or keep them on the project and read them back with readPersonas(project.meta.personas). Four ship with the stories that had them in the prototype: 🌊 Tushell and 🦉 Wise Owl with Tushell and the Orchestra of Agents, 🌀 Miawapaskone and 🪶 Grandmother Yazhi with the Keeper Chronicles.

The bundled stories

/fixtures ships nine documents in two families.

miadi — the six teaching narratives the prototype was built around, each with every beat toned: The Catalyst of Change (the seed: a story about learning to write with soul, inside a tool for writing with soul), Echoes of the Past (the smallest complete lattice, the one to read first), Tushell and the Orchestra of Agents, The Keeper Chronicles: The Initiation, The Bridge Walker, The Archive Keeper.

Catalyst is assembled by scripts/build-miadi-fixtures.mjs: its source document is written in the design vocabulary and carries no tones, while the prototype held each beat's appreciation, narrative function and tone triad in a separate table. The script merges them and records what it merged. The prototype's own adapter is deliberately not used — it dropped 22 of the 23 moments.

ncp — the protocol repository's own examples: the minimal canonical story, The Shawshank Redemption (which does not validate clean, and says why), and Anora.

Where the canonical data comes from

assets/ncp-schema.json, assets/terminology.json and assets/examples/*.json are copied from the Narrative Context Protocol repository (MIT). They are re-synced, with a source path and sha256 stamped into the generated files:

npm run sync      # sync-schema + sync-docs
npm test          # build, then node --test

This package is a consumer of that protocol, never its author. When the schema changes there, the generated enums, required lists, and term maps change here — and the stamp says which version is in.