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

@workspacejson/spec

v0.5.0

Published

JSON Schema and TypeScript types for workspace.json

Readme

@workspacejson/spec

JSON Schema and TypeScript types for workspace.json v0.4.

This is the canonical specification package. The schema it ships is the normative one — there is exactly one copy, and downstream repositories materialize it from this package rather than maintaining their own.

Source of truth: workspacejson/standard. Publication authority for this package currently sits with the historical repository it was extracted from; see the repository's release-authority note.

Install

npm install @workspacejson/spec

Validate a document

npx workspacejson-spec validate .agents/workspace.json

Exits 0 on a valid document, non-zero otherwise. validate <file> is the only command; there is no --help flag, and any other invocation exits non-zero with usage.

API

Validation

import { validate, validateV4, validateLegacy, version } from '@workspacejson/spec';

console.log(version); // derived from the packaged manifest, e.g. '0.4.4'

validate(doc);        // true if doc is a valid v0.3 or v0.4 document
validateV4(doc);      // true if doc is a valid v0.4 document
validateLegacy(doc);  // true if doc is a valid v0.1/v0.2 document

Path identity — stored keys

Per ADR-006: a path-bearing value in a workspace.json is data, not a command. It must already be a canonical repository-root-relative POSIX key. Two functions decide that — one for a single key, one for a whole document — and neither ever produces or repairs a key.

import { validateStoredKey, inspectStoredKeys } from '@workspacejson/spec';
import type {
  StoredKeyResult,
  StoredKeyRejection,
  StoredKeyDocument,
  StoredKeySurface,
  StoredKeyFinding,
} from '@workspacejson/spec';

validateStoredKey(rawKey) — one key

validateStoredKey('src/a.ts');    // { valid: true,  key: 'src/a.ts' }
validateStoredKey('src/../a.ts'); // { valid: false, reason: 'dotdot-segment' }
type StoredKeyResult =
  | { readonly valid: true;  readonly key: string }
  | { readonly valid: false; readonly reason: StoredKeyRejection }

type StoredKeyRejection =
  | 'empty' | 'nul' | 'unpaired-surrogate'
  | 'unc-prefix' | 'drive-letter' | 'backslash' | 'absolute-posix'
  | 'leading-dot-slash' | 'dot-segment' | 'dotdot-segment'
  | 'repeated-separator' | 'trailing-separator'

Pure, total, filesystem-free and deterministic. Apply it to the string as stored, before any path library sees it — normalizing first and validating second is the defect ADR-006 records, because by then src/../a.ts is already a.ts and nothing ever asked whether it was well-formed.

There is deliberately no repaired-key field. A valid result carries the input unchanged; a rejection carries a reason and nothing you could mistake for a usable key. A malformed key matches nothing — including the value normalization would have produced.

Reason precedence is fixed, so two implementations classify the same key identically: cannot-be-a-string first, then cannot-be-POSIX, then merely non-canonical. C:\x reports drive-letter rather than backslash; ./x reports leading-dot-slash rather than dot-segment.

Case and Unicode form are significant: A.ts and a.ts are two keys, and so are the NFC and NFD spellings of café.ts. A genuine U+FFFD is a valid pathname character — distinguishing it from a substitution needs the original bytes, which is a producer concern.

canonicalizeHostQuery is not part of this package. Turning a host or editor path into a key needs a filesystem and a proven repository root; ADR-006 §10 assigns it to integrations and hosts.

inspectStoredKeys(document) — one document

Reports every malformed key on every ratified path-bearing surface. This is the report it half of ADR-006 §9; decline to match it stays with you, since only you know what a lookup is.

if (!validate(raw)) {
  // Existing invalid-document handling.
} else {
  for (const finding of inspectStoredKeys(raw)) {
    console.warn(`${finding.pointer}: ${finding.rawKey} — ${finding.reason}`);
  }
}
type StoredKeyDocument = WorkspaceJsonV3 | WorkspaceJsonV4

type StoredKeySurface =
  | 'generated.fileIndex'
  | 'generated.coChange[].files[]'
  | 'generated.fragility[].file'
  | 'manual.fragileFiles[].path'

type StoredKeyFinding = {
  readonly pointer: string          // RFC 6901, with `~0` / `~1` escaping
  readonly surface: StoredKeySurface
  readonly rawKey: string           // exactly as stored
  readonly reason: StoredKeyRejection
}

The input is a schema-validated document, not unknown — call validate() first, as above. That narrowing is what makes the result unambiguous: [] means every inspected value in an accepted document is well-formed, and an unvalidated value is outside the declared input domain rather than silently "clean". inspectStoredKeys does not call validate() for you and is not a second document validator.

Findings are location-bearing records: one per occurrence, never deduplicated, never repaired. The same malformed spelling at four locations is four findings. Order carries no meaning — match on pointer, never on position.

manual.coChangePatterns is not inspected. ADR-003 amendment A-005 has not ratified its item shape — the schema constrains items to {"type": "object"} and nothing more — so inspecting a presumed files field would turn an authoring-time assumption into a normative contract. The surface is added once A-005 settles it.

v0.4.x acceptance is unaffected. validate() and validateV4() still accept artifacts that carry malformed keys, because a v0.4.x reader is required to report and decline to match while continuing over the well-formed remainder. Rejecting such a document is a v0.5 document-profile change and has not happened.

Schema object

import { workspaceJsonSchema } from '@workspacejson/spec';

// workspaceJsonSchema.$id === 'https://workspacejson.dev/schema/v1.json'
// workspaceJsonSchema.title === 'workspace.json'

TypeScript types

import type {
  WorkspaceJsonV3,
  WorkspaceJsonV4,
  CoChangeEntry,
  FragilityEntry,
  FrameworkEntry,
  FileIndexEntry,
  IntelligenceState,
} from '@workspacejson/spec';

WorkspaceJsonV3 describes the v0.3 four-property shape:

const doc: WorkspaceJsonV3 = {
  manual: {},
  generated: {
    specVersion: '0.3',
    generatedAt: new Date().toISOString(),
    by: { name: 'my-tool', version: '1.0.0' },
    frameworkManifest: [],
    fileIndex: {},
  },
  agents: {},
  health: { intelligenceState: 'INSUFFICIENT_DATA', observationCount: 0, confidence: 0 },
};

WorkspaceJsonV4 extends v0.3 with formally typed co-change and fragility arrays:

const doc: WorkspaceJsonV4 = {
  manual: {},
  generated: {
    specVersion: '0.4',
    generatedAt: new Date().toISOString(),
    basisRevision: '3c9a0f14b7e25d8613af04c2e9b7d5081f6a2c3d',
    by: { name: 'my-tool', version: '1.0.0' },
    frameworkManifest: [],
    fileIndex: {},
    coChange: [
      // Observation form — what a new producer emits. `generated` is optional:
      // omit it unless you implement a deterministic classifier.
      { files: ['src/auth.ts', 'src/session.ts'], support: 8, occurrences: 24 },
      { files: ['pnpm-lock.yaml', 'package.json'], support: 196, occurrences: 204, generated: true },
    ],
    fragility: [
      { file: 'src/auth.ts', changeCount: 34, revertCount: 8, revertRate: 0.24, fragilityScore: 0.82, excluded: false },
    ],
  },
  agents: {},
  health: {
    intelligenceState: 'CONFIDENT',
    observationCount: 1247,
    confidence: 0.87,
    workflowFragility: 0.5,
    codebaseHealth: 0.7,
    changeVolatility: 0.4,
  },
};

Consumer guidance for coChange: filter on generated === true to skip tooling-coupled pairs (lockfiles, package manifests) and surface only real source couplings.

The flag is optional in the observation form and three-state — true, false, and absent, meaning the producer performed no classification and asserts nothing. It is a classification rather than an observation: it cannot be read off the commit graph, and this standard specifies no portable deterministic classifier, so a producer without one omits it rather than guessing. Never collapse absent into false — if (!entry.generated) reads an unclassified pair as a confirmed source coupling. Branch on undefined separately. The flag remains required in the deprecated legacy form. See ADR-003 A-010.

Two entry forms during the v0.4 transition. Exactly one applies to any entry, and a reader must establish which before reading the numbers:

| Form | Carries | Status | | -- | -- | -- | | Observation | support + occurrences | What new producers emit | | Legacy | rate + occurrences | Deprecated; accepted so published artifacts stay valid |

Both in one entry is invalid; neither is invalid. Narrow with entry.support !== undefined — the types model this as a union with ?: never members, so an entry carrying both is a compile error too.

for (const e of doc.generated.coChange ?? []) {
  if (e.support !== undefined) {
    const ratio = e.occurrences > 0 ? e.support / e.occurrences : undefined;
  } // else: legacy entry — e.rate, on the older denominator
}

In the observation form, support is the distinct qualifying commits in which both files changed and occurrences the distinct qualifying commits in which at least one did. Both count commits, not file events, and both are symmetric: files is an unordered pair, so swapping its two entries changes neither. Derive support / occurrences yourself where occurrences > 0 — nothing derived is stored, so a new commit perturbs the counts rather than rewriting every derived value in the file.

In the legacy form, occurrences carries the pre-amendment meaning, which was never normatively specified and must not be assumed symmetric. Never compare occurrences across the two forms. rate is removed at the next document-profile change; until then it stays valid.

The array is homogeneous — every entry legacy, or every entry observation. A mixed array is rejected. Observation-form occurrences has a minimum of 1, so support / occurrences is always defined and no conforming artifact can produce 0/0, NaN or infinity; a pair never observed changing has no entry at all. Legacy occurrences keeps its original minimum of 0.

generated.basisRevision names the revision the observation-form counts were taken over: a full-length lowercase Git object name (40 hex characters for SHA-1, 64 for SHA-256), declared once for the whole section. The requirement and the pattern apply only where an entry uses the observation form. Everywhere else the key is unconstrained, so a legacy artifact already carrying basisRevision: "HEAD" stays valid — constraining it globally would have invalidated a document that was valid before this amendment.

A producer emitting the observation form must declare it whenever coChange exists, including when the array is empty. That case is a producer obligation, not a schema rule, because an empty array carries no discriminator. For a reader the four states are:

| Shape | Means | | -- | -- | | coChange absent | Not analyzed | | coChange: [], no pin | Legacy / unknown — not evidence of zero | | coChange: [], pinned | Analyzed at that revision; no qualifying pairs | | pin ≠ current revision | Stale observation |

One invariant is not expressible in JSON Schema and is enforced by validate() instead: support <= occurrences, since commits where both files changed are a subset of commits where at least one did. An implementer validating with a bare JSON Schema validator will not catch a producer that violates it.

Consumer guidance for fragility: filter excluded: false before ranking. Entries with excluded: true are generated or lock files with fragilityScore: 0.

Producer-conformance contract

workspace.json deliberately separates human evidence from generated observations. Producers preserve manual verbatim across regeneration and replace the producer-owned generated, agents, and health sections. Human annotations for a producer belong under manual; consumers must not rely on generated sections remaining unchanged after a run.

Producers should write only when their material projection changes. Timestamps identify the last material generation, not merely the last command invocation.

JSON Schema file

The raw JSON Schema is available via the ./schema export:

import schema from '@workspacejson/spec/schema' with { type: 'json' };

Materialize it from this package and hash-check it. Do not maintain an editable second copy — that is how copies drift.

The schema declares $id: https://workspacejson.dev/schema/v1.json and is served at that URL. The www. host also serves the schema, but the bare domain is canonical. See ADR-005 for the reconciliation record.

The v1 in the filename is a legacy artifact of the file's original naming, not a claim that the format is at version 1.0. The current document profile is v0.4.

Contents

  • schema/v1.json — published JSON Schema (draft-2020-12)
  • src/schema.ts — TypeScript const mirroring the schema
  • src/types.ts — TypeScript types for v0.3, v0.4, and legacy v0.1/v0.2
  • src/index.ts — validate(), validateV4(), validateLegacy(), version, type re-exports

Migration from v0.3 to v0.4

v0.4 adds two always-present arrays to generated and formally types three health fields. v0.3 documents remain valid — v0.4 is a strict superset.

{
  "generated": {
    "specVersion": "0.4",
    "basisRevision": "3c9a0f14b7e25d8613af04c2e9b7d5081f6a2c3d",
    "coChange": [],
    "fragility": []
  },
  "health": {
    "workflowFragility": 0.5,
    "codebaseHealth": 0.7,
    "changeVolatility": 0.4
  }
}

Check generated.specVersion === "0.4" or use validateV4(doc) before accessing these fields.

Migration from v0.1/v0.2

v0.3 replaces the flat top-level shape with four required sections:

{
  "manual": {},
  "generated": {
    "specVersion": "0.3",
    "generatedAt": "...",
    "by": { "name": "...", "version": "..." }
  },
  "agents": {},
  "health": {}
}

Use validateLegacy(doc) to detect v0.1/v0.2 documents; use validate(doc) for v0.3/v0.4.

Requirements

Node.js >= 20.

Further reading

Full documentation lives in the source repository:

License

Apache-2.0.