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

@game-infra/event-schemas

v1.1.4

Published

Narrative event-editor API contract: event definitions, board collaboration, AI generation/review/polish schemas + toad-contracts endpoint definitions

Readme

@game-infra/event-schemas

Narrative event-editor API contract: valibot schemas plus toad-contracts defineApiContract definitions for event definitions, the Miro-style event board (HTTP + live-collab WebSocket protocol), and the AI generation/review/polish/tier-review endpoints. This is the largest schema slice extracted from golem-forge; it builds on @game-infra/api-schemas-core (response wrappers, AI provider enum) and @game-infra/common-data-model (tier-review entity types).

Published to npm. Also consumable as sibling source (exports points at src/).

Install

// pnpm sibling checkout
"@game-infra/event-schemas": "link:../../game-infra/packages/schemas/event-schemas"
// or from npm
"@game-infra/event-schemas": "^0.1.0"

valibot is a peer dependency (^1.4.0); the consumer provides it.

Usage

Contracts carry method, path, params, and request/response schemas in one object. Build a URL with pathResolver, derive the route pattern with mapApiContractToPath:

import {
  eventDefinitionLoadContract,
  aiReviewEventContract,
  TierReviewRequestSchema,
} from "@game-infra/event-schemas";
import { mapApiContractToPath } from "@toad-contracts/valibot";

eventDefinitionLoadContract.pathResolver({ gameId: "g1", eventId: "e1" });
// '/games/g1/event-definitions/e1'
aiReviewEventContract.pathResolver(); // '/ai/review-event'
mapApiContractToPath(eventDefinitionLoadContract); // '/games/:gameId/event-definitions/:eventId'

Register on a Hono service with @toad-contracts/hono (buildHonoRoute) and call from a frontend with @toad-contracts/frontend-http-client (sendByApiContract) — both drive off the same contract objects. The event board's live traffic is not HTTP: eventBoardLayoutWsContract is only the upgrade endpoint, and the actual edits flow through the ClientCollabMessageSchema / ServerCollabMessageSchema op protocol validated on the WebSocket.

Text that reads differently per player

An event's prose is normally one fixed string, which forces an author who wants "you have been here before" to write a second event. ConditionalTextDTO assembles it at read time instead, out of fragments carrying the same preconditions a choice is gated on:

import {
  type ConditionalTextDTO,
  flattenConditionalText,
  resolveConditionalText,
} from "@game-infra/event-schemas";

const greeting: ConditionalTextDTO = {
  fragments: [
    {
      id: "greeting",
      text: "The smith looks up at a stranger.",
      variants: [
        {
          conditions: [{ type: "HasMet", params: { characterId: "tallow" } }],
          text: "The smith looks up and knows your face.",
        },
      ],
    },
    {
      id: "the-debt",
      text: "She has not forgotten what you owe her.",
      conditions: [{ type: "FlagSet", params: { flag: "owes-the-smith" } }],
    },
  ],
};

resolveConditionalText(greeting, (condition) => myGame.holds(condition));
flattenConditionalText(greeting); // the whole text as written, for anything that cannot evaluate

A fragment whose conditions fail is left out; otherwise the first variant whose conditions hold replaces its text, which is therefore the "else" wording. Nothing here evaluates a precondition — which terms exist is a game's own vocabulary, so the predicate is an argument.

It hangs off EventDefinitionDTO and EncounterStageDTO as conditionalDescription, and off ChoiceDefinitionDTO as conditionalName / conditionalDescription. Every one of those fields is optional and sits beside the plain string it refines; by convention the plain field holds flattenConditionalText of the conditional one, so a consumer that does not understand fragments still shows the whole authored text. The event editor ships a ConditionalTextEditor for authoring them.

Strict validation is built per game, not shipped

Preconditions, activations and consumable conditions travel as { type, params }. The DTOs here are lenient — any type, any params — because a shared service stores events, it does not adjudicate them.

A game wants more, so it builds the strict schemas from its own vocabulary:

import { createStrictEventSchemas } from "@game-infra/event-schemas";
import { createCodecRegistry, commonActivationCodecs } from "@game-infra/common-data-model";
import { parse } from "valibot";

const strict = createStrictEventSchemas({
  preconditions: [...commonPreconditionCodecs, ...myGameCodecs.preconditions],
  activations: [...commonActivationCodecs, ...myGameCodecs.activations],
  consumableConditions: myGameCodecs.consumables,
});

parse(strict.eventDefinition, incomingEvent);

A TypeCodec from common-data-model already carries type and paramsSchema, so it satisfies EventTermSpec structurally — pass the codec arrays you already have. An omitted or empty kind rejects every term of that kind.

Returned schemas are GenericSchema, since their shape depends on the terms passed in. Name a type from your own call: InferOutput<typeof strict.eventDefinition>.

Migrating from the shipped strict schemas

ModifiableStatSchema, StrictActivationDTOSchema, StrictPreconditionDTOSchema, StrictConsumableConditionDTOSchema, StrictOutcomeConfigDTOSchema, StrictChoiceDefinitionDTOSchema, StrictEncounterStageDTOSchema, StrictEventDefinitionDTOSchema and the per-term schemas behind them (ModifyStatActivationSchema, LearnSpellActivationSchema, SpecificLecturePreconditionSchema, CastSpellConsumableSchema, …) are removed.

They enumerated one game's vocabulary — strength, mana, maxSneakiness, LearnSpell, SpecificLecture — in a package every game shares, so a second game could only adopt those terms or ignore strict validation entirely. Replace them with createStrictEventSchemas and the terms your game registers; the shapes it returns are the same, with the vocabulary yours.

EventRaritySchema was union([string("common"), string("uncommon"), …]), which reads like an enum but is not one: valibot's string(message) takes an error message, not a literal, so it accepted any string. It is now spelled string() — identical at runtime. Re-tighten it in your own layer if you want your tiers enforced.

Extension points

  • Tier-review entity vocabulary: TierReviewEntityTypeSchema is a plain string(). golem-forge hardcoded picklist(['persona','item','secret','spell','organization']); that vocabulary is game-specific, so games re-tighten it via the editor's tierReviewEntityTypes config. The entityType field in tierReviewAIOutputSchema likewise dropped its enum so each game defines its own types.
  • AI provider: the provider field reuses AIProviderSchema from @game-infra/api-schemas-core.

Migrating from golem-forge

The old EventApiPaths constants and PathResolvers / resolve*Path helpers (including the internal-tools EventGenerationPathResolvers and NextScreenPathResolvers) are gone. Get a URL from the contract itself with contract.pathResolver(params) and the :param route pattern with mapApiContractToPath(contract). Prefer driving requests through @toad-contracts/frontend-http-client (sendByApiContract) and routes through @toad-contracts/hono (buildHonoRoute), which consume the contract directly so you never touch a raw path.

Dependencies

@game-infra/api-schemas-core (createSuccessResponseSchema, TimestampFieldsSchema, ApiResponseSchema, AIProviderSchema), @game-infra/common-data-model (re-exported EntityInfo / AvailableEntities types), @toad-contracts/core, @toad-contracts/valibot; peer valibot.

Consumers

event-editor (frontend), core-service (event-board Durable Object plus the event-definition, board-layout, and AI endpoints).