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

affordance-spec

v0.1.0

Published

A schema-validated spec for affordance maps — nodes, edges, and an engagement overlay — plus an exported validator and a reference force-directed viewer.

Downloads

39

Readme

affordance-spec

A schema-validated spec for mapping a codebase and the affordances that hang off it. Its foundation (L1) is an affordance map — the pages, API routes, storage buckets, DB tables, components, external services, and infrastructure of a codebase, plus the relationships between them and an auth/access overlay — and an engagement overlay (L2) keys affordances and flows onto those nodes.

This repo is three things:

  1. Two specsAFFORDANCE-MAP-SPEC.md (L1, the static catalog) and ENGAGEMENT-SPEC.md (L2, users flowing through it), each the prose companion to a canonical Zod schema (src/schema.ts, src/engagement-schema.ts → TypeScript types → runtime validation → published JSON Schemas under schema/).
  2. An exported validator — published as the affordance-spec package, so any producer (e.g. a static scanner) can validate its output against the contract.
  3. A reference viewer — an interactive, force-directed renderer for any conforming map. It ships a small synthetic dataset so it runs out of the box.

The map data is produced by a producer (a scanner or generator) that emits a document conforming to the schema. Producers are codebase-specific and live outside this repo; the schema is the contract between them and any consumer.

How the two specs relate

L1 describes the possible (what exists); L2 describes the actual (recorded sessions) and the intended (authored flows). Their overlap is deliberately narrow:

  • ResourceID — L1's durable node identity (AFFORDANCE-MAP-SPEC.md §5) is the id space L2 keys on, but loosely: alignment is a re-runnable projection, never a stored foreign key, so L1 can regenerate without invalidating recordings.
  • Route templates — both use templates, never concrete URLs (page:/items/[id], not /items/123), so the two layers share one id space.
  • Envelope conventions — both documents carry a literal version with the same bump policy, ISO-8601 UTC timestamps, and L2's app names the L1 source it targets.

Two ideas make a map readable at scale:

  • Infrastructure vs. services. A dependency consumed near-universally (e.g. the database, used by 150+ nodes) is reclassified from an external service into its own infra category, so the genuinely-external integrations stay legible instead of drowning in plumbing edges.
  • Isolated nodes. Once infrastructure carries its own edges, a node with no edges at all is interesting — a static page, a presentational component, or a pure-logic route. These render as hollow dashed rings and can be spotlighted from the sidebar's ⊘ Isolated chip.

Interactive docs

Three sidecar Vite sites render the spec (in each: npm install && npm run dev, or npm run build for a portable single-file dist/index.html):

  • spec-docs/the unified reference: both specs on one page, the overlap made explicit (with a live alignment demo), validators for all three documents, and a downloadable LLM-markdown flattening of the whole spec.
  • schema-docs/ — the L1 map contract in depth.
  • engagement-docs/ — the L2 engagement contract in depth.

Quick start

npm install
npm run dev        # open the reference viewer (renders the bundled demo data)

To produce a single, self-contained dist/index.html (works offline, no server):

npm run build
# open dist/index.html

Scripts

| script | what it does | | ------------------ | ------------------------------------------------------------------- | | npm run dev | Vite dev server for the reference viewer | | npm run build | Type-check + bundle the viewer to a single-file dist/index.html | | npm run schema | Emit the JSON Schemas under schema/ from the Zod source | | npm run build:lib| Build the validator package (dist-lib/, JS + .d.ts) | | npm run typecheck| tsc --noEmit |

Using the validator

A producer depends on the package and validates its output before writing it:

import { parseAffordanceMap, type AffordanceMap } from 'affordance-spec';

const map: AffordanceMap = parseAffordanceMap(draft); // throws on any violation

parseAffordanceMap() validates at runtime — including referential integrity (every edge endpoint must be a real node). The engagement overlay (L2) is exported the same way (parseEngagementLog, parseAffordanceCatalog).

The bundle — one file, both specs

The artifacts keep three lifecycles (generated map / captured recordings / curated catalog), but for transport a spec bundle (BUNDLE-SPEC.md, src/bundle-schema.ts, parseSpecBundle) wraps any subset of them in a single JSON an importer can sniff by its kind:

{
  "version": 1,                          // envelope version (parts version themselves)
  "kind": "spec-bundle",                 // discriminator for importers
  "bundledAt": "2026-07-05T12:00:00.000Z",
  "app": "example-app",                  // optional, informational
  "map": { /* AffordanceMap */ },        // optional
  "engagement": { /* EngagementLog */ }, // optional — sessions[] holds many recorded journeys
  "catalog": { /* AffordanceCatalog */ } // optional — flows[] holds many named flows
}

At least one part is required. Each part validates against its own schema, unchanged, and multiplicity lives inside the parts: engagement.sessions is an array (merge several recordings into one log rather than shipping several bundles) and catalog.flows is an array (a whole library of named flows can travel with the affordances their steps reference). Because the bundle is the one place the artifacts travel together, it also cross-checks them: a catalog bundled with a map must reference nodes that map actually has (ENGAGEMENT-SPEC.md §8); recordings stay exempt — they re-align by projection.

For tooling, npm run schema also emits a single combined JSON Schema, schema/affordance-spec.schema.json — every document type as a named definition (#/definitions/AffordanceMap, EngagementLog, AffordanceCatalog, SpecBundle as root), so one file covers everything a tool needs to validate.

Bringing your own data

The viewer renders src/data/affordance-map.json, which ships as a synthetic example-app. To view a real codebase, point a producer at it and drop the resulting (schema-valid) document in that slot — it is plain data, so a local override needn't be committed.

The schema

src/schema.ts is the single source of truth (Zod). From it:

  • TypeScript types (AffordanceMap, Node, Edge, …) are inferred.
  • parseAffordanceMap() validates data at runtime.
  • npm run schema emits the portable JSON Schema.

Shape

{
  "version": 1,
  "generatedAt": "2026-01-01T00:00:00.000Z",
  "source": "example-app",
  "nodes": [
    { "id": "page:/dashboard", "type": "page", "label": "/dashboard", "route": "/dashboard",
      "auth": "auth", "file": "src/app/dashboard/page.tsx", "counts": { "apis": 1 } },
    { "id": "/api/items", "type": "api", "label": "items", "verbs": ["GET", "POST"],
      "auth": "auth", "file": "src/app/api/items/route.ts",
      "counts": { "tables": 1, "buckets": 0, "services": 0 } },
    { "id": "table:items", "type": "table", "label": "items" },
    { "id": "infra:Database", "type": "infra", "label": "Database", "consumers": 7 }
  ],
  "edges": [
    { "from": "page:/dashboard", "to": "/api/items", "kind": "page-api" },
    { "from": "/api/items", "to": "table:items", "kind": "api-table" },
    { "from": "/api/items", "to": "infra:Database", "kind": "api-infra" }
  ],
  "summary": { "pages": 1, "apis": 1, "components": 0, "tables": 1,
               "buckets": 0, "services": 0, "infra": 1, "edges": 3 }
}

type is a discriminated union — page/api/component carry extra fields (route, verbs, auth, counts); table/bucket/service are bare nodes; infra carries a consumers count (its universality).

Source layout

src/
  schema.ts            canonical Zod schema + types + validation
  engagement-schema.ts L2 engagement overlay schema
  index.ts             package entry — re-exports schemas, types, validators
  theme.ts             palette, type/auth metadata
  model.ts             GraphModel: positions, adjacency, neighbourhoods
  dom.ts               tiny HTML/SVG element helpers
  graph/
    simulation.ts      dependency-free force-directed layout
    view.ts            pan/zoom transform
    renderer.ts        SVG element management + per-frame draw
  ui/
    sidebar.ts         search, type filters, grouped node list
    detail.ts          selected-node metadata + connections
  main.ts              wiring: data -> model -> sim/renderer/ui + interactions
  data/
    affordance-map.json  bundled synthetic demo data
scripts/
  emit-schema.ts       Zod -> JSON Schema

Notes

The viewer renders whatever validates against the schema. auth levels and counts are producer-derived — a directional overlay, not a security audit — and are only as accurate as the producer that emitted them.