Skip to content

feat(world-data): hidden world data and story events - #101

Merged
ackness merged 3 commits into
mainfrom
feat/hidden-world-data
Oct 1, 2026
Merged

ackness merged 3 commits into
mainfrom
feat/hidden-world-data

Conversation

@ackness

@ackness ackness commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

Add a framework-level way for world packages to ship content that must stay hidden until the story reaches it, and a plugin that reveals such content from world state. This is the first phase of #97.

Hidden world data (visibility: hidden)

  • worldData sources gain visibility: public | hidden (default public, so existing packages are unchanged).
  • Hidden sources may only target a data contract (to: contract:<id>). They import into the receiving plugin's reserved _hidden.<namespace> bucket. Lorebook projection (+lorebook), characters, world metadata, media, and indexTo are rejected, and world projections skip hidden sources.
  • One framework-enforced boundary covers every exit:
Exit Behavior
Public plugin-data API, /state panel, discovery Hidden rows omitted; single reads return 404
LLM plugin-data-get / plugin-data-list Never return hidden data
input.inject (kind: plugin-data) Rejects hidden namespaces with an explicit error
Extension handlers (prompt.segment@1, ui.slot@1, session.world-context@1) ctx.pluginData cannot read hidden namespaces
Generic plugin writes Cannot rewrite them (reserved _ prefix)

Only the receiving plugin's runtimes can read hidden data. This is spoiler protection, not encryption: the files remain in the local world package and in browser-local workspace checkpoints, which is documented.

story-events plugin

  • A model-free pre-turn function runtime reads hidden events, the frozen dimension snapshot (world.dimensions@1), and world time (world-time-context@1).
  • Conditions combine all / any / not over dimension paths and locale-independent numeric time fields with equals, notEquals, in, gte, gt, lte, lt, exists.
  • At most one event is revealed per turn (priority, then ID). Events fire once by default or repeat after cooldownTurns; a retried turn re-delivers the same cue; a rolled-back execution does not consume the event.
  • The reveal is delivered as a story-event-cue@1 output, consumed by narrator and chat-mode-narrator through a new optional storyEvent input. A public revealed record stores only the event ID, optional public title, and turn numbers, never the payload.

Bundled worlds

Every bundled world now enables story-events and ships hidden events whose conditions use its own dimensions and world time:

  • Haruka Academy: three heroine route moments (Mio's club-room secret, Kaho's unfinished verse, Akane's sister's note).
  • Mistport: Frost hearing the fog during the deep withdrawal; Iron Meg revealing the archive fragment.
  • Emberback: a second echo near Crownfire; Eli admitting what he heard in the static.
  • Lantern Barrow: the singing reaching the village, Corvin cornered with the Lantern Heart, and the Barrow King waking.

Payloads are narrator briefs that stage a moment without resolving the main mysteries early.

Type of change

  • New feature (feat)
  • Bug fix (fix)
  • Refactor (refactor)
  • Documentation (docs)
  • Infra / CI (chore)
  • Performance (perf)
  • Breaking change (BREAKING CHANGE)

Verification

  • pnpm lint, pnpm check:i18n, pnpm deps:check, pnpm release:preflight (4 worlds), prettier and oxlint on changed files.
  • pnpm validate:plugin for story-events, narrator, and chat-mode-narrator.
  • Tests: server 1691 passed / 4 skipped, web 1543, runtime 1491, shared 234, tools 176, context 125, world-init 14, story-events 13.
  • New end-to-end test (apps/server/tests/api/story-events-hidden.test.ts) runs two real turns through /api/actions with real plugins and a temporary world: before the condition holds, no model prompt and no public API contains the payload; on the turn it holds, only the narration call receives it, and the stored reveal record stays payload-free.
  • New unit tests for hidden source import and rejected targets, the LLM plugin-data tools, input.inject, extension handlers, and the plugin's condition, selection, and retry logic.
  • Every bundled event validates against story.events@1, references only existing dimension paths (unmet with no diagnostics at the initial state, met when its conditions are applied), and imports into _hidden.events for both zh-CN and en-US sessions without appearing in public APIs.
  • Not run: real-model play reaching an event in each world.

Related issue / context

Docs sync

  • docs/reference/world-data.md (visibility field, hidden data section, bundled worlds), plugins.md, tools.md, api.md, extension-points.md, prompt-structure.md, docs/architecture/storage.md.
  • plugins/story-events/README.md and the create-world skill.

World packages can declare a worldData source as `visibility: hidden`
(default `public`). Hidden sources may only feed a data contract and
import into the receiver's reserved `_hidden.<namespace>` bucket; they
cannot project into the lorebook, characters, world metadata, or media,
and world projections skip them.

Only the receiving plugin's runtimes can read hidden data. Public
plugin-data APIs, the state panel, discovery, extension handlers, the
LLM plugin-data tools, and `input.inject` all exclude or reject it, and
generic plugin writes cannot rewrite it.

Refs #97
Add the story-events plugin: a model-free pre-turn runtime that reads
hidden events, the frozen dimension snapshot, and world time, and
reveals at most one event per turn as a `story-event-cue@1` output.
Conditions combine all/any/not over dimension paths and numeric time
fields; events fire once by default or repeat after a cooldown, and a
retried turn re-delivers the same cue. The public reveal record never
stores the payload.

Both narrators consume the cue through an optional `storyEvent` input.
An end-to-end test proves the payload stays out of every prompt and
public surface until its turn, then reaches only narration.

Refs #97
Each bundled world now ships hidden events through story-events:
heroine route moments in Haruka Academy, Frost's and Iron Meg's scenes
in Mistport, a second echo and Eli's confession in Emberback, and three
GM encounters in Lantern Barrow. Conditions only reference each world's
own dimensions and world time; payloads are narrator briefs that do not
resolve the main mysteries early.

Refs #97
@ackness
ackness merged commit 0d2d4cf into main Oct 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant