Repository navigation
RISE SDK
Status: PROVISIONAL v1, 1.0.0-provisional (RISE_SDK_VERSION in src/core/rise-sdk.js). Written 2026-10-08 for CC-008 of the Creative Control stage (design, §15). Owner: Claude coordinator (plugin lane). It freezes after the owner's Live Run, the stage's acceptance reading in a host. Until then any part may change, but only together with this document, in the same change.
Earlier records (SDK-001 and SDK-002 in docs/product/tasks/, and the Composer decision) said the SDK should wait for observed integration demand. The owner reopened it with the Creative Control stage on 2026-10-08: the parameter manifests and the scene protocol are now real, and this document writes them down as one contract. It does not claim that an outside integrator exists yet.
RISE turns a document called a Current into a timed reading: a voice, the words shown as they are spoken, and pictures behind them. This document is for an engineer who has never seen RISE and wants to do one of three things:
- Write Currents as another model provider or tool: the shape of the document, every field, every limit, every refusal (§3 to §6).
- Write a generated scene: a small JavaScript module that draws a picture RISE runs in an isolated worker (§7), or a figure: an SVG document RISE admits and shows as an image (§3.9).
- Host RISE in another MCP host, or embed it: the two tools, the resources, and what the card tells the host's model (§8).
What RISE draws, how it lays out text and which pixels appear are not part of the contract (§10).
Every value below comes from a named constant in the code. src/core/rise-sdk.js re-exports the contract's constants in one module, as data only. src/core/rise-sdk.test.js checks that every number, name and list in this document equals the code. So if a limit changes, an engine gains a parameter, rise.lib gains a call or a message is added, that test fails until this document is updated.
After the freeze, 1.x changes are additive only: a new optional field, a new engine, parameter, named cue, style, face, place, size or sound, a new rise.lib call, a new message a receiver may ignore, or a looser limit. Anything else is a new major version of RISE_SDK_VERSION: removing or renaming anything, tightening a limit, changing what a value means, or making an optional field required. A breaking change to the document also takes a new schema id (rise.current.v3), and a breaking change to the worker protocol a new SCENE_PROTOCOL_VERSION. A v1 or v2 Current that is valid at the freeze stays valid in every 1.x.
| Constant | Value | Meaning |
|---|---|---|
RISE_SDK_VERSION |
1.0.0-provisional |
This contract. |
RISE_CURRENT_SCHEMA |
rise.current.v1 |
The first Current: passages (RISE-CURRENT-V1-SLICE.md). Still valid. |
RISE_CURRENT_SCHEMA_V2 |
rise.current.v2 |
The Current with beats and scenes (§3). |
SCENE_PROTOCOL_VERSION |
1 | Messages between the card and a scene worker (§7.10). Sent in scene/init. |
PROTOCOL_VERSIONS |
2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05
|
MCP versions the server speaks, newest first (worker/mcp-server.mjs). |
PROTOCOL_VERSION |
2026-01-26 |
The MCP Apps extension version the card says hello with (src/live/hosts/mcp-port.js). |
A Current is one JSON object. RISE validates it strictly: it refuses anything it does not recognise and never clamps a value or drops a field. The reference validator is validateRiseCurrent in src/core/rise-current.js. It is a pure function: the server runs it, and the card runs it again. A refusal is a RiseCurrentError with a code, a JSON path ($.beats[3].hold.ms) and a message.
{
"schema": "rise.current.v2",
"id": "vector-length",
"title": "How long is a vector?",
"theme": "cobalt",
"origin": { "kind": "model", "name": "Your name", "provider": "Who runs you" },
"scenes": [{ "id": "field", "engine": "attractor" }, { "id": "calm", "engine": "still" }],
"beats": [
{ "say": "A vector has a direction and a length.", "scene": "field", "sound": "starlight" },
{ "hold": { "ms": 2500 } },
{ "say": "Its length is the square root of x squared plus y squared.", "show": "Its length is $\\sqrt{x^2+y^2}$.", "place": "caption" },
{ "show": "The Pythagorean theorem, in two dimensions.", "hold": { "ms": 2000 }, "scene": "calm", "size": "smaller" }
]
}| Field | Required | Value |
|---|---|---|
schema |
yes |
rise.current.v2. |
id |
yes | A trimmed id of at most RISE_CURRENT_LIMITS.id characters. |
title |
yes | Nonblank text of at most RISE_CURRENT_LIMITS.title characters. |
origin |
yes |
{ kind, name, provider? }, where kind is model or human. A model origin must name its provider; a human one must not. name and provider hold at most RISE_CURRENT_LIMITS.name characters. It says who wrote the words. It is not a citation. |
theme |
no | One of RISE_CURRENT_THEME_IDS. A name only: colours come from RISE's own palettes. |
look |
no | One of RISE_CURRENT_LOOKS: a preset of field, face, size and theme. |
style |
no | One of RISE_CURRENT_STYLES (§4). |
type |
no |
{ text?, caption? }: the reading's faces, each one of BEAT_TYPES (§5). |
scenes |
no | At most BEAT_LIMITS.scenes scenes (§3.4). |
beats |
yes | 1 to BEAT_LIMITS.beats beats, in order (§3.2). |
A field that is not listed is refused. So is a null optional field (leave it out). A v2 Current with segments is refused: passages belong to v1.
Which fields a beat has decides its kind:
| Kind | Has | Spoken | Shown | Lasts |
|---|---|---|---|---|
| say |
say, optional show, no hold
|
say |
show if given, else say; nothing with place: "none"
|
as long as the voice takes |
| hold |
hold, no say, no show
|
nothing | the running scene | §3.3 |
| show |
show and hold, no say
|
nothing | show |
hold.ms, shared by its words |
Any other combination is refused (BEAT_KIND).
| Beat field | Required | Value |
|---|---|---|
say |
no | Text the voice says, at most BEAT_LIMITS.text characters. |
show |
no | Text shown instead of say, at most BEAT_LIMITS.text characters. May hold maths (§5). |
hold |
no |
{ ms, maxMs? }, whole milliseconds. ms from BEAT_LIMITS.holdMinMs to BEAT_LIMITS.holdMaxMs; maxMs at least ms and at most BEAT_LIMITS.holdMaxMs. |
scene |
no | The id of a declared scene, started at this beat. It keeps running under the following beats until another starts. |
cue |
no | A signal to the running scene when this beat begins (§6.3, §7.4): matches BEAT_CUE_PATTERN, at most BEAT_LIMITS.cue characters. Refused with no scene running. |
transition |
no |
{ ms }, 0 to BEAT_LIMITS.transitionMaxMs, how long the new scene takes to fade in. Default BEAT_LIMITS.transitionDefaultMs. |
place |
no | One of BEAT_PLACES (§5). |
size |
no | One of BEAT_SIZES (§5). |
type |
no | One of BEAT_TYPES: a face for this beat (§5). |
emphasis |
no | At most BEAT_LIMITS.emphasis words, each nonblank and at most BEAT_LIMITS.emphasisLength characters, to set apart. The voice is unchanged. |
sound |
no | A sound id: an atmosphere or music bed that stays (soundscape), a one-off tone, or none. The ids are the enum of the tool's input schema (SOUND_IDS in src/audio/sound-ids.js). |
A beat that starts a scene and cues it reaches the scene it started. Text may not contain the playback markers [PAUSE], [FLASH], [HOLD], | or U+E000 (CURRENT_RESERVED_TEXT).
- A hold with no
maxMslasts exactlyms. - A hold with
maxMs, under a generated scene that exportsreportsCompletion = true, ends when the scene callsrise.done(). It never ends sooner thanSCENE_LIMITS.earliestDoneMsinto the hold, and it ends atmaxMsif the scene never callsdone. - Any other hold lasts
ms, a hold under a figure (§3.9) included. So does a hold whose scene fails or is frozen: it then runs onmsfrom where it began. - Pause stops the voice, the hold and the scene's time together.
A scene is a native engine with parameters, generated code, or a figure (an SVG document, §3.9): one of the three. Code beside an engine is SCENE_CODE; an svg beside anything else is SCENE_SVG. Ids are unique within the Current.
| Scene field | Required | Value |
|---|---|---|
id |
yes | A trimmed id of at most BEAT_LIMITS.id characters. |
engine |
no | One of SCENE_ENGINES (§6). Given with params, never with code. |
params |
no | The engine's parameters, each held to its manifest (§6.2). Unknown names and out-of-range values are refused. |
code |
no | A generated scene's module: at most BEAT_LIMITS.code bytes of UTF-8, with export default (§7). |
svg |
no | A figure: one SVG document of at most BEAT_LIMITS.svg bytes of UTF-8, admitted by its rules (§3.9). Never with engine, params or code. |
| Constant | Value | Meaning |
|---|---|---|
BEAT_LIMITS.beats |
64 | Beats per Current. |
BEAT_LIMITS.scenes |
8 | Scenes per Current. |
BEAT_LIMITS.code |
24,576 | UTF-8 bytes of one scene's code. |
BEAT_LIMITS.svg |
32,768 | UTF-8 bytes of one figure's svg. |
BEAT_LIMITS.text |
4,000 | Characters of one say or show. |
BEAT_LIMITS.totalText |
20,000 | Characters of say and show in all. |
BEAT_LIMITS.id |
120 | Characters of a scene id. |
BEAT_LIMITS.cue |
40 | Characters of a cue. |
BEAT_LIMITS.emphasis |
8 | Emphasised words per beat. |
BEAT_LIMITS.emphasisLength |
40 | Characters of one emphasised word. |
BEAT_LIMITS.holdMinMs |
200 | Shortest hold. |
BEAT_LIMITS.holdMaxMs |
60,000 | Longest hold, and the most maxMs may be. |
BEAT_LIMITS.transitionMaxMs |
5,000 | Longest scene transition. |
BEAT_LIMITS.transitionDefaultMs |
600 | The transition a beat that sets none gets. |
RISE_CURRENT_LIMITS.id |
120 | Characters of the Current's id (and a v1 segment's). |
RISE_CURRENT_LIMITS.title |
200 | Characters of the title. |
RISE_CURRENT_LIMITS.name |
120 | Characters of origin.name and origin.provider. |
MCP_CURRENT_BYTES |
65,536 | UTF-8 bytes of the whole Current as JSON, through the MCP host path (src/live/hosts/mcp-size.js). |
BEAT_CUE_PATTERN |
^[A-Za-z0-9_:=.-]+$ |
What a cue may be; the input schema uses the same pattern. |
| Constant | Value | Meaning |
|---|---|---|
RISE_CURRENT_THEME_IDS |
classic, amethyst, prism, ember, cobalt, jade, rose, citrine, silver
|
Colour themes. |
RISE_CURRENT_LOOKS |
plain, gallery, nocturne, garden, flame, signal, iris, revel, vigil, inlay
|
Looks. |
RISE_CURRENT_STYLES |
premium-educational, open-field
|
Styles (§4). |
SCENE_ENGINES |
still, attractor, genesis, living-flame, fractal, turrell, neural, rockgarden, harmonograph, ostensoria, apparitio
|
Native engines (§6). |
| Code | Refused when |
|---|---|
CURRENT_OBJECT |
Something that must be a plain object is not. |
CURRENT_UNKNOWN_FIELD |
A field is not in the contract (or is __proto__, constructor, prototype). |
CURRENT_ID |
An id is empty, untrimmed or too long. |
CURRENT_DUPLICATE_ID |
Two scenes share an id. |
CURRENT_TEXT |
Text is blank, not a string, or too long. |
CURRENT_RESERVED_TEXT |
Text holds a playback marker. |
CURRENT_TOTAL_TEXT |
All the text together is over BEAT_LIMITS.totalText. |
CURRENT_THEME |
theme is not a theme id. |
CURRENT_LOOK |
look is not a look id. |
CURRENT_STYLE |
style is not a style id. |
CURRENT_ORIGIN |
origin.kind is neither model nor human. |
CURRENT_PROVIDER |
A model origin has no provider, or a human one has one. |
SCENE_COUNT |
scenes is not an array of at most BEAT_LIMITS.scenes. |
SCENE_ENGINE |
engine is not a native engine. |
SCENE_PARAM |
A parameter is unknown to the engine, or outside its bounds. |
SCENE_CODE |
A scene has both engine and code, or code that is too large or has no export default. |
SCENE_SVG |
A figure's svg is not text, is over BEAT_LIMITS.svg, or stands beside engine, params or code. |
BEAT_COUNT |
beats is missing, empty, or too long. |
BEAT_KIND |
A beat's fields fit no kind (§3.2). |
BEAT_HOLD |
hold.ms or hold.maxMs is out of bounds. |
BEAT_SCENE |
scene names no declared scene. |
BEAT_CUE |
A cue does not match the pattern, is too long, has no running scene, is not one the running engine takes, or goes to a figure, which takes none. The message lists the cues an engine takes. |
BEAT_TRANSITION |
transition.ms is out of bounds. |
BEAT_PLACE |
place is not a place. |
BEAT_SIZE |
size is not a size. |
BEAT_TYPE |
type (on a beat or in the Current's type) is not a role or face. |
BEAT_EMPHASIS |
emphasis has too many words, or a blank or long one. |
BEAT_SOUND |
sound is not a sound id. |
An unknown schema is refused too. The server adds two refusals that are not codes: a Current over MCP_CURRENT_BYTES, and a scene its admission refuses (§7.7, and §3.9 for a figure).
A rise.current.v1 Current is passages (segments), each with text, an optional visual from RISE_CURRENT_VISUALS and optional Dive notes. It is accepted unchanged. Its contract is RISE-CURRENT-V1-SLICE.md. Its limits:
| Constant | Value | Meaning |
|---|---|---|
RISE_CURRENT_VISUALS |
still, attractor, genesis
|
The visual a v1 passage may name. |
RISE_CURRENT_LIMITS.segments |
16 | Passages per v1 Current. |
RISE_CURRENT_LIMITS.segmentText |
4,000 | Characters of one passage. |
RISE_CURRENT_LIMITS.totalText |
20,000 | Characters of all passages. |
RISE_CURRENT_LIMITS.dives |
8 | Dive notes per passage. |
RISE_CURRENT_LIMITS.diveText |
600 | Characters of one Dive note. |
A figure is a picture the model draws as SVG: { "id": "triangle", "svg": "<svg …>…</svg>" }. It is for a picture that is right standing still, such as a labelled diagram; a picture that changes on cue is a generated scene (§7).
-
Contract. One SVG document of at most
BEAT_LIMITS.svgbytes. It takes no cues: a beat whosecuereaches a figure is refused (BEAT_CUE), and a hold under it lasts itsms. It is drawn behind the words like any scene, and the reading's own field returns when the next scene starts, or when the figure is refused or cannot be drawn. -
On the score. A figure lowers to the visual cue
{ "kind": "scene", "sceneId": "<id>", "svg": "<svg …>" }: the scene cue carriessvgorcode, exactly one. Offline rendering does not support a scene cue of either kind. -
Ink.
currentColorinside an image resolves in the image's own document, so the card writes the theme's ink onto the figure's root ascolor, unless the root sets one. Draw ink incurrentColor. -
Motion. SMIL (
<animate>and its kin) runs inside the image, in reduced motion too; this version does not pause it. - On a phone. In a host's card 640 px wide or less, a figure (like a generated scene) is fitted whole above a strip kept for three lines of the words, so the words never cover it; a card under 350 px tall lays the words over its foot instead.
Admission. admitSvg in src/core/svg-admission.js reads the text with a small tokenizer, never a DOM, and never draws it. The Worker runs it before it accepts a Current, and the card runs it again before it draws a figure; a figure the card refuses is not mounted. The rules:
- At most
BEAT_LIMITS.svgbytes of UTF-8. - After an optional
<?xml …?>, whitespace and comments, the document begins with<svg, and it ends with</svg>: once the root has closed, nothing but whitespace and comments may follow (the figure is not well-formed: nothing may follow the root <svg>). Every element closes in order, and no attribute is given twice. The root carriesxmlns="http://www.w3.org/2000/svg"(without it no browser draws the image) and aviewBox. - No DOCTYPE, no
<!ENTITY, no other<!declaration, no processing instruction but the xml declaration, and no CDATA that holds<. - Only these elements, named exactly; anything else,
script,foreignObject,image,a,iframe,object,embed,video,audioandfeImageamong them, is refused by name.
-
Elements a figure may use:
svg,g,path,rect,circle,ellipse,line,polyline,polygon,text,tspan,textPath,defs,linearGradient,radialGradient,stop,clipPath,mask,pattern,marker,symbol,use,title,desc,animate,animateTransform,animateMotion,set,mpath,style,filter,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence
- No attribute whose name begins with
on, in any case, and noexternalResourcesRequired. -
hrefandxlink:hrefonly when the value begins with#. An animation'sattributeNamemay not behref,xlink:hrefor an event handler. -
xmlnsonly as the SVG namespace, andxmlns:xlinkonly as XLink; no other namespace, since a prefix could rename an element. - No value, with its character references decoded and its whitespace removed, containing
javascript:,data:,http:,https:or//, norurl(unless it isurl(#. - In CSS, a
styleattribute or a<style>(which holds only CSS): the same, and no@import,expression(,behavior,-moz-bindingor escape (\).
The card then shows the admitted text only as an <img> of a blob: URL (image/svg+xml), centred and scaled to fit. In a host's card the picture ends where the bar's room begins, so a figure, like a scene's canvas, is fitted above the controls and never runs under them. An SVG image runs no script and fetches nothing by platform rule: that is the second lock. The URL is let go once the image loads or the figure is taken down.
A refused figure refuses the whole rise_present call, as a refused code scene does (§7.7): one line per problem, up to MAX_SCENE_LINES across all scenes. For example:
Scene "triangle" was refused: line 2, column 3: <script> is not an element a figure may use.
Repair the figure and call rise_present again with the whole Current.
When the card refuses a figure, or the image cannot be drawn, it says so in RISE's words (§8.4):
scene "triangle": not drawn — the card refused the figure: "line 2, column 3: <script> is not an element a figure may use"
scene "triangle": not drawn — the figure could not be drawn as an image
A style is a named bundle (STYLES in src/core/styles.js). It gives a place, size and faces to every beat that shows text and sets none of its own, and rise.lib defaults to every generated scene. A hold takes no typography. Faces set on the Current or on a beat override the style's, role by role. A model reads a style's full guidance with the rise_guide tool (§8).
| Style | Place | Size | Text face | Caption face | Ease | Stroke | Grid alpha |
|---|---|---|---|---|---|---|---|
premium-educational |
caption |
as-set |
book-serif |
humanist-sans |
smooth |
2.5 | 0.12 |
open-field |
centre |
as-set |
— | display-serif |
out |
2 | 0.25 |
Each style also sets a label font for rise.lib, a CSS font string naming one of RISE's faces. A Current with no style gets the library's own defaults:
| Constant | Value | Meaning |
|---|---|---|
LIBRARY_DEFAULTS.ease |
smooth |
The tween curve a call leaves out. |
LIBRARY_DEFAULTS.stroke |
2 | Line width in CSS pixels. |
LIBRARY_DEFAULTS.gridAlpha |
0.25 | Opacity of grid. |
LIBRARY_DEFAULTS.labelFont |
14px sans-serif |
Font of label. |
Places. BEAT_PLACES says where a beat's text goes:
| Constant | Value | Meaning |
|---|---|---|
BEAT_PLACES |
centre, caption, top, left, right, none
|
centre is the reading's own line; caption is the lower third; top is the upper third; left and right are a column, which a narrow screen shows as a caption; none is spoken and not shown. |
BEAT_SIZES |
smaller, as-set, larger, display
|
Steps from the size the reader chose: one down, none, one up, two up. Never past the reader's smallest or largest size. |
TYPE_ROLES |
book-serif, display-serif, humanist-sans, geometric-sans, mono, display, handwritten, condensed
|
Faces by what the text is for. The guide teaches roles. |
BEAT_TYPES |
book-serif, display-serif, humanist-sans, geometric-sans, mono, display, handwritten, condensed, crimson-pro, instrument-serif, instrument-sans, space-grotesk, jetbrains-mono, marcellus, caveat, barlow-condensed, inter
|
What type may name: a role, or a face id. |
Faces. TYPE_FACES in src/core/typography.js. RISE serves every face itself (public/fonts), because a host's card may load fonts only from RISE's origin.
| Face | Role | Weight |
|---|---|---|
crimson-pro |
book-serif |
400 |
instrument-serif |
display-serif |
400 |
instrument-sans |
humanist-sans |
400 |
space-grotesk |
geometric-sans |
500 |
jetbrains-mono |
mono |
400 |
marcellus |
display |
400 |
caveat |
handwritten |
500 |
barlow-condensed |
condensed |
500 |
inter |
— | 400 |
A beat's type wins. Without one, a beat placed at centre takes the Current's (or style's) type.text, and a beat placed elsewhere takes type.caption. When nothing names a face, the reader's own face setting stands.
Maths. In show, $…$ is inline maths and $$…$$ displayed maths. RISE typesets it with its own copy of KaTeX (trust: false), loaded only when a reading has maths. Until it loads, the formula shows as its TeX source. The voice says say, so write the spoken form there ("x squared").
Each native engine has a manifest (SCENE_MANIFESTS in src/scenes/manifests.js). The same manifests drive the validator, the score, the cue delivery and the guide the model reads (describeManifests()).
{
id: 'attractor', // what a scene's "engine" names
kind: 'field', // 'still' | 'field' (persistent engine) | 'procedural' (pattern engine)
surface: 'attractor', // what a cue command addresses
readableOverText: true, // the engine is quiet enough to read words over
parameters: {
system: { type: 'enum', values: ['aizawa', 'thomas', 'halvorsen'], default: 'aizawa', cueable: false },
intensity: { type: 'number', minimum: 0.4, maximum: 0.75, default: 0.65, cueable: true }
// type 'integer' is a number that must be whole
},
cues: { calm: { intensity: 0.45 }, bright: { intensity: 0.75 } } // named cue -> parameters it sets
}A parameter that is cueable changes while the engine runs. Every other parameter applies only when the scene starts. The visual-control path has an older manifest of the same shape for the attractor (ATTRACTOR_VISUAL_MANIFEST in src/core/visual-control-contract.js), which also carries requiresCanvas: true. Nothing reads requiresCanvas today, and the scene manifests do not carry it.
| Engine | Kind | Surface | Readable over text | Named cues |
|---|---|---|---|---|
still |
still |
still |
yes | — |
attractor |
field |
attractor |
yes |
calm (intensity=0.45), bright (intensity=0.75) |
genesis |
field |
genesis |
yes | — |
living-flame |
field |
living-flame |
yes |
calm (energy=0.15), surge (energy=0.9), warm (hue=30), cool (hue=-40) |
fractal |
procedural |
fractal |
yes | — |
turrell |
procedural |
turrell |
yes | — |
neural |
procedural |
neural |
yes | — |
rockgarden |
procedural |
rockgarden |
yes | — |
harmonograph |
procedural |
harmonograph |
yes | — |
ostensoria |
procedural |
ostensoria |
yes | — |
apparitio |
procedural |
apparitio |
yes | — |
| Engine | Parameter | Type | Range or values | Default | Cueable |
|---|---|---|---|---|---|
attractor |
system |
enum |
aizawa, thomas, halvorsen
|
aizawa |
no |
attractor |
palette |
enum |
white, red, blue, gold, purple, neon, jade, rose, citrine, silver
|
gold |
no |
attractor |
form |
enum |
mirror, kaleido, bilateral
|
mirror |
no |
attractor |
intensity |
number | 0.4 to 0.75 | 0.65 | yes |
attractor |
speed |
number | 0.25 to 4 | 1 | no |
attractor |
trace |
enum |
filament, swarm
|
filament |
no |
genesis |
preset |
enum |
harmonic, chaotic, twittering, architectural, gravitational
|
harmonic |
no |
living-flame |
preset |
enum |
ember-cathedral, violet-nebula, glacial-silk, solar-bloom, verdant-current, prismatic-knot
|
ember-cathedral |
no |
living-flame |
energy |
number | 0 to 1 | 0.35 | yes |
living-flame |
complexity |
number | 0 to 1 | 0.55 | yes |
living-flame |
hue |
number | -180 to 180 | 0 | yes |
living-flame |
symmetry |
integer | 1 to 8 | 1 | no |
living-flame |
intensity |
number | 0 to 1 | 0.35 | no |
harmonograph |
climate |
enum |
auto, emberDawn, solarFlare, midnightWater, stormViolet, jadeVeil, whiteHeat
|
auto |
no |
harmonograph |
pendulums |
enum |
lateral, rotary
|
lateral |
no |
ostensoria |
palette |
enum |
auto, iris, reliquary, ember, ice, verdant, lilac, teal, sepia, peacock, rose, citrine
|
auto |
no |
apparitio |
palette |
enum |
auto, prism, marian, ember, holo
|
auto |
no |
Engines not listed in the second table take no parameters. A params object for them must be empty or left out.
A beat's cue under a native scene is one of two forms. Anything else is refused (BEAT_CUE), and the message names the cues the engine takes.
-
A named cue from the engine's
cues, such ascalm. -
set:<parameter>=<value>on acueableparameter, such asset:hue=30. The value is a decimal number (an optional minus sign and fraction, no exponent) and must be within the parameter's bounds.
The card lowers a cue to commands of the shape { surface, parameter, value }, one per parameter it sets, and delivers them when the beat begins. That shape is checked again where it is applied (validateVisualCommand). A renderer that implements this contract takes the same commands.
A generated scene is a picture the model writes as code. RISE checks the code on the server without running it (§7.7). In the card, it runs the code in a dedicated Web Worker on an OffscreenCanvas behind the words. Generated scenes are promised for Claude only. They are not promised for ChatGPT until its sandbox has been measured (§10).
code is the text of one ES module (ES2022). Its single default export is a function. It receives rise and returns an object with frame(t, dt) and, optionally, cue(name, { instant }). This is the example the guide gives every model:
export const reportsCompletion = true;
export default function scene(rise) {
const { lib } = rise;
const plane = lib.axes({ x: [-1, 4], y: [-1, 3] });
const v = lib.vector({ x: 3, y: 2, label: 'v', t: 0 });
return {
frame(t, dt) {
lib.clear();
lib.grid(plane);
plane.draw();
v.draw(plane);
},
cue(name, { instant }) {
if (name === 'draw') return lib.tween(v, { t: 1 }, { ms: 900, instant });
if (name === 'turn') return lib.tween(v, { angle: Math.PI / 2 }, { ms: 1500, instant }).then(() => rise.done());
}
};
}- The default export runs once. It must return an object with a
framefunction, or the scene fails atinit. -
frame(t, dt)draws the whole picture each time it is called. -
cue(name, { instant })is optional. It may return a promise; a rejected promise counts as a failure. -
export const reportsCompletion = true(a boolean literal, nothing else) declares that the scene callsrise.done(). Without it,doneis ignored and a scene cannot end a hold by accident (§3.3). -
input(event)is optional, and called only in a scene that listens (§7.11).export const listens = ['pointer', 'keys'](either or both) declares that it listens, asrise.listendoes.
| Member | What it is |
|---|---|
ctx |
The canvas's 2D context, created the first time it is read. |
use('webgl2') |
Switch to WebGL2 instead. Call it before the first frame and before reading ctx. It returns the context. |
gl |
The WebGL2 context after use('webgl2'), otherwise null. |
size |
{ width, height, dpr }: CSS pixels of the canvas (in a host's card, the field above the bar's room), and the device pixel ratio, capped at SCENE_LIMITS.maxDpr. The same object is updated on resize. |
theme |
The reading's colours: background, text, accent, muted, highlight. |
reducedMotion |
true when the reader asked for reduced motion. |
fonts |
An empty list in this version. Labels draw in the system's faces. |
lib |
The drawing library (§7.3), with the style's defaults. |
done() |
Says the current hold may end (§3.3). |
listen(...kinds) |
Says the scene listens to the reader's 'pointer', 'keys', or both (also as one array). Before the first frame only; anything else throws (§7.11). |
emit(control, value) |
Says what the reader set, while the scene handles their input (§7.11). Never throws; what is not taken is refused with a reason. |
Coordinates: axes returns a frame, the mapping from scene units to device pixels that the other calls draw on. label takes CSS pixels.
| rise.lib | Signature | What it does |
|---|---|---|
clear |
clear(color?) |
Fill the canvas with the background (or color). First in every frame. |
axes |
axes({ x: [x0, x1], y: [y0, y1] }, { color, labels, margin }?) |
A frame with draw(), toX(x), toY(y), origin, and the range. |
grid |
grid(frame, { step, color, alpha }?) |
Grid lines at every step. |
plot |
plot(frame, fn or points, { color, width, samples, to }?) |
A curve of x => y or [[x, y], …]; tween to (0 to 1) to draw it. |
vector |
vector({ x, y, from, color, label, t, angle, width }) |
An arrow; returns its state with draw(frame). Tween t (how much is drawn) and angle (radians). |
point |
point(frame, [x, y], { color, radius, label }?), or point({ id, x, y, draggable: true, label, frame? })
|
A dot; or, given one object with an id, a point the reader drags (§7.11). |
line |
line(frame, [x0, y0], [x1, y1], { color, width, dash }?) |
A segment. |
arc |
arc(frame, [cx, cy], radius, from, to, { color, width }?) |
An arc; angles in radians. |
polygon |
polygon(frame, [[x, y], …], { color, alpha, outline }?) |
A filled shape. |
label |
label(text, { at: [x, y], color, font, align, baseline, alpha }) |
Canvas text at CSS pixels. |
tween |
tween(target, { property: value }, { ms, ease, instant }?) |
Moves numbers on RISE's clock; returns a promise. With instant, or reduced motion, it lands at once. |
tick |
tick(dt) |
Advances every tween. The worker calls it before each frame; a scene does not. |
ease |
ease.linear, smooth, in, out, inOut
|
Easing curves, t => t. |
color |
color(name, alpha?) |
A theme colour by name, with an alpha. |
palette |
palette |
The theme colours by name. |
moving |
moving |
How many tweens are still running. |
slider |
slider({ id, x, y, w, min, max, value, step?, label }) |
A horizontal slider the reader moves, at CSS pixels; .value is its number, to its step (§7.11). |
step |
step({ id, steps, index, label, x?, y? }) |
A step-through: back and on targets either side of its label; steps a count or a list of labels; .value the index (§7.11). |
focusable |
focusable(...widgets) |
The order the number keys choose widgets in; the default is the order they were made (§7.11). |
The card owns time. It sends frame messages only while the reading plays, one at a time: it waits for each frame to finish before it sends the next. t is scene time in milliseconds and stops while the reading is paused. dt is the time since the last frame, cut short after a long gap, so that a hidden tab does not make the scene jump. Before the first visible frame the worker runs frame(0, 0) once. An error there fails the scene before anything is shown. A cue is delivered when the beat that names it begins. Cues sent before the scene is ready wait in order.
instant is part of the protocol: a scene must land on a cue's end state at once when instant is true. The card sends instant: false in this version. Replaying earlier cues instantly on Replay or seek (design §7) is not built.
With rise.reducedMotion true, every rise.lib tween completes in one step. A scene that animates by its own arithmetic should also honour the flag. Holds still last their time.
A scene has no network, no storage, no DOM, no timers of its own, no way to speak for the worker, and no way to build code from strings. The names are declared once, in BANNED_SCENE_NAMES (src/scenes/scene-bans.js), and enforced by two locks:
-
Shadowed in the worker and refused by admission:
fetch,XMLHttpRequest,WebSocket,EventSource,WebTransport,BroadcastChannel,Worker,SharedWorker,importScripts,indexedDB,caches,navigator,setTimeout,setInterval,requestAnimationFrame,postMessage,FontFace,eval,Function,self,globalThis -
Refused by admission only:
window
The first list is SHADOWED_GLOBALS. Before it loads the code, the worker replaces each of those names with a stub that throws and cannot be put back. FontFace is there because its load() fetches a URL. The second list is STATIC_ONLY_NAMES: names that are no global of a worker, so the parse refuses them and there is nothing to shadow. The worker's own channel (addEventListener, removeEventListener, onmessage, onerror, close) is locked the same way once its listener is attached, because the global reaches a scene without being named, as a listener's this or currentTarget. The worker also seals the constructor of every function prototype, because [].constructor.constructor reaches Function without naming it. If any name cannot be shadowed or any prototype sealed, the code is not run.
The sandbox does not rest on the host's policy. In the card, two lines of defence hold without it: the card's own Content Security Policy (cardPolicy in src/live/hosts/mcp-card.js, with no 'unsafe-eval', inherited by the blob: worker) refuses code built from strings and any script or fetch from an origin but RISE's, and the worker's runtime lock above leaves nothing usable on the global a scene can reach.
Why: the code is untrusted. It runs with none of RISE's authority: no host port, no storage, no fonts, no speech, and no network beyond what the host's sandbox would block anyway. Time comes only from frame, so that Pause stops the picture.
admitSceneCode in src/core/scene-admission.js parses every code scene with acorn as an ES2022 module and never runs it. The rules:
- At most
BEAT_LIMITS.codebytes of UTF-8. - The code parses as a module. A module is strict, so
withis a syntax error. - Exactly one default export, a plain function (not async, not a generator).
- No
importof any kind, noimport(), noexport … from. - No
debugger. - No banned name used as a name, anywhere. A local variable with a banned name is refused too. A property name such as
a.fetchor{ fetch: 1 }is allowed. - A
reportsCompletionexport, if present, is a boolean literal.
One refused scene refuses the whole rise_present call (isError). The text has one line per problem: up to MAX_SCENE_LINES lines across all scenes, sorted by position. Each line reads Scene "<id>" was refused: line L, column C: <rule>., and the column counts from one. A final line asks the model to repair the code and call again. For example:
Scene "vector" was refused: line 2, column 16: `fetch` is not available to a scene, and the name is refused even as a local name.
| Constant | Value | Meaning |
|---|---|---|
SCENE_LIMITS.frameSoftMs |
24 | A frame slower than this is slow. |
SCENE_LIMITS.frameSoftCount |
3 | This many slow frames within one second is a failure. |
SCENE_LIMITS.frameHardMs |
200 | One frame slower than this is a failure. |
SCENE_LIMITS.silenceMs |
1,000 | A worker that has not answered a frame for this long is a failure. |
SCENE_LIMITS.earliestDoneMs |
200 | The soonest into a hold that done may end it. |
SCENE_LIMITS.lumaEveryMs |
100 | How often, in scene time, the worker samples brightness for the flash gate. |
SCENE_LIMITS.maxDpr |
2 | The device pixel ratio a scene draws at, at most. |
SCENE_LIMITS.inputPointerPerSecond |
120 | Pointer inputs a listening scene is given a second; moves beyond it are coalesced, the latest kept (§7.11). |
SCENE_LIMITS.inputKeysPerSecond |
30 | Key inputs a listening scene is given a second; keys beyond it are dropped. |
SCENE_LIMITS.emitPerSecond |
10 | What the reader set, taken from one control a second; the trailing value is kept. |
SCENE_LIMITS.emitControls |
64 | Distinct controls, and widgets, one scene may have. |
SCENE_LIMITS.emitAfterInputMs |
1,000 | An emit is taken only this soon after an input the card delivered. |
SCENE_LIMITS.controlLength |
40 | A control's id, at most, of [a-z0-9-]. |
SCENE_LIMITS.valueLength |
80 | A string value, at most. |
SCENE_LIMITS.labelLength |
40 | A widget's label as the card mirrors it, at most. |
An exception in loading, init, a frame, a cue or an input is also a failure. On failure the card terminates the worker and shows the reading's fallback visual (the look's field, or a still) for that scene's beats, and it does not run that scene again in that reading. Any open hold runs on its ms. The failure is reported (§8.4).
Photosensitivity. The worker samples its own picture every SCENE_LIMITS.lumaEveryMs (an 8×8 average brightness) and reports it to the card. The card feeds the samples to the same flash gate every RISE visual answers to (VisualFlashGate). A scene that would flash is frozen on its last frame, its holds run on ms, and the freeze is reported. A scene cannot opt out.
The worker is RISE's built src/scenes/scene-worker.js, started by src/scenes/create-scene-worker.js: a classic worker from a blob: URL in a host's card; RISE's own module worker on its pages. In a card the page's origin may be opaque, and a module worker's script is fetched in a mode a blob:null URL cannot pass, so there it would never load. Either way the worker imports the admitted code with import() from a second blob: URL. It holds the transferred canvas and has nothing else of the page.
Messages are plain objects with a type. A receiver drops any message whose type it does not know, and reads a known one field by field. Nothing the worker sends is run by the card.
| Message | Type | Fields |
|---|---|---|
TO_WORKER.init |
scene/init |
version (SCENE_PROTOCOL_VERSION), code, width, height, dpr, theme, reducedMotion, library (the style's defaults), canvas (transferred) |
TO_WORKER.frame |
scene/frame |
t, dt (milliseconds) |
TO_WORKER.cue |
scene/cue |
name, instant
|
TO_WORKER.resize |
scene/resize |
width, height, dpr
|
TO_WORKER.input |
scene/input |
kind, x, y, buttons, key, modifiers (§7.11); sent only to a scene that listens to its kind |
TO_WORKER.dispose |
scene/dispose |
none |
TO_HOST.ready |
scene/ready |
reportsCompletion, and listens ('pointer', 'keys') only for a scene that listens |
TO_HOST.frameDone |
scene/frame-done |
t, ms (how long the frame took) |
TO_HOST.cued |
scene/cued |
name |
TO_HOST.done |
scene/done |
none |
TO_HOST.luma |
scene/luma |
value (0 to 1), t
|
TO_HOST.emit |
scene/emit |
changes ([control, value], at most SCENE_LIMITS.emitControls), refused ([control or null, reason], at most 8) |
TO_HOST.widgets |
scene/widgets |
widgets ({ id, kind, label, value, rect } each, rect [x, y, w, h] in CSS pixels), only from a scene that listens |
TO_HOST.error |
scene/error |
message, where (scene.js:line:column or null), phase (load, init, frame, cue or input) |
A scene may give the reader something to move, and say what they set (plan §20). Events go into the sandbox and out of it; the sandbox is not opened.
Listening. A scene declares it once, before its first frame: export const listens = ['pointer', 'keys'], or rise.listen('pointer', 'keys') in its default export. A scene that does not is given no input at all: no scene/input is posted to its worker, its canvas lets every press through and stays hidden from assistive technology, and its messages are as before. A scene that listens to keys gets them only while its canvas has the focus, which only the reader gives it (a press, or Tab), with a visible ring.
What comes in. { type, x, y, buttons, key, modifiers }, frozen, made by the card from the DOM event's numbers and never the event: type one of pointerdown, pointermove, pointerup, pointercancel, keydown, keyup; x, y in the canvas's CSS pixels, clamped to it (null for a key); buttons 0 to 31; key an arrow or one printable character (null for a pointer); modifiers { shift, alt, ctrl, meta }. Never the instrument's keys: Space, r, R, f, F, -, _, =, +, [, ] and Escape stay the reading's (Escape returns the focus to the reading's controls), and a key with Ctrl, Alt or Meta is the browser's. Pointer moves beyond SCENE_LIMITS.inputPointerPerSecond are coalesced, the latest kept; presses and releases are never dropped. Keys beyond SCENE_LIMITS.inputKeysPerSecond are dropped. While the reading is paused, an input draws one frame at the same scene time (dt 0), sampled for the flash gate like any other.
What goes out. rise.emit(control, value): control matches [a-z0-9-]{1,40}; value is a finite number, a boolean, or a string of at most SCENE_LIMITS.valueLength characters. It is taken only while the scene handles the reader's input (its widgets, or its own input), so a scene cannot say the reader did what they did not. The latest value per control is sent after each input, at most SCENE_LIMITS.emitControls controls a scene. The card reads it again: within SCENE_LIMITS.emitAfterInputMs of an input it delivered, at most SCENE_LIMITS.emitPerSecond a second per control with the trailing value kept. Anything else is dropped and noted once per control and reason (control, value, controls, unprompted), at most 16 a scene. Nothing is sent on a timer. In RISE Live, what the reader set reaches the model with their next words, as "set w1 in the scene network to 0.7"; in a host's card it is kept and goes nowhere.
The widgets. slider, point({ id, … }) and step (§7.3) draw themselves with draw() in frame(), take the presses, drags and arrow keys the worker hands them, keep their value (.value), and emit it on change: a slider its number, a point "x, y", a step what the reader sees (its label, or "2 / 5"). A value the scene sets is not the reader's and emits nothing. A press takes the widget under it (the last made wins) until release. A focused scene's arrows adjust the widget last pointed at, or the first in focusable order: a slider or a step by one step, a point by a fiftieth of its frame; the number keys 1 to 9 choose another, which is ringed. An id outside [a-z0-9-]{1,40}, a repeated id, more than SCENE_LIMITS.emitControls widgets, or a slider whose min is not below its max, throws at creation. The card mirrors the widgets in a hidden list (aria-live="off"), says each move in the reading's live region, and holds touch only over each widget's region, so a swipe elsewhere still scrolls the page.
Where it is taught. RISE Live's instructions teach it. The guide a host's model reads (rise_guide, the tool's description) does not yet; a scene that listens runs in the card all the same.
RISE is an MCP server at MCP_PATH on its own origin (worker/mcp-server.mjs). It uses Streamable HTTP in its simplest form: every request is a POST with one JSON body and gets one JSON answer, with no stream and no session. It keeps nothing, calls no model and holds no key. It is off unless switched on. How it was checked, and against what, is in LIVE-MCP.md.
| Constant | Value | Meaning |
|---|---|---|
MCP_PATH |
/api/mcp |
The endpoint. |
TOOL_NAME |
rise_present |
The tool that presents a Current. |
GUIDE_TOOL_NAME |
rise_guide |
The tool that returns a style's guidance. |
APP_URI |
ui://rise/current |
The card the host shows for a rise_present call. |
APP_MIME |
text/html;profile=mcp-app |
The card's type. |
MAX_SCENE_LINES |
10 | Scene refusal lines in one answer. |
-
Input:
{ current }and nothing else.currentisoneOfthe v1 and v2 schemas (currentJsonSchema,currentJsonSchemaV2). Every limit and catalog in the schema comes from the validator's constants. Where JSON Schema cannot say what the validator checks, the schema is looser, never stricter. -
Accepted: the text
RISE accepted this Current for presentation to the reader.andstructuredContent: { current }. That means it is accepted, not that it has played. -
Refused:
isError: truewith text a model can act on:RISE refused this Current: <message> (<path>). Correct it and call rise_present again., a size refusal, or the scene lines of §7.7. An argument besidecurrentis refused by name. - Annotations: read-only, not destructive, idempotent, closed world. No authentication.
- The description holds the guide to writing a Current, generated from the same constants, and one line per style.
-
Input:
{ style }, one ofRISE_CURRENT_STYLES, and nothing else. -
Result: that style's full guidance and two worked Currents, as one text block. Anything else gets
isErrorand the list of styles. It exists because some hosts (claude.ai) let only the reader attach resources, and the model can only call tools.
-
ui://rise/current(text/html;profile=mcp-app): the card. Its_meta.ui.cspnames only RISE's origin. -
ui://rise/guide/<style>(text/markdown): the same textrise_guidereturns, for hosts whose model reads resources. Any other guide address is-32002.
When a generated scene fails in the card, the card sends ui/update-model-context with one text block, if the host offered hostCapabilities.updateModelContext.text when the card said hello (Claude does). The host keeps only the latest update, so each one carries every line so far:
RISE could not run part of the reading it is showing; the reader sees its fallback instead:
scene "vector": frame — the scene’s own words: "TypeError: v.draw is not a function" at scene.js:14:5
scene "vector": frozen — it would flash more than three times a second; its last frame stays
The lines are RISE's words. A scene's own error message is quoted as data, with control characters removed and double quotes replaced, so a scene cannot write instructions into the model's context. A failed send costs nothing, because the reader already sees the fallback.
| Constant | Value | Meaning |
|---|---|---|
PORT_LIMITS.report |
2,000 | Characters of one update. |
PORT_LIMITS.reportLine |
400 | Characters of one line. |
PORT_LIMITS.reports |
5 | Updates per Current. |
The same lines go to the browser console as [RISE scene].
-
The document carries its version in
schema:RISE_CURRENT_SCHEMAandRISE_CURRENT_SCHEMA_V2. A reader of Currents accepts both. -
The worker protocol carries
SCENE_PROTOCOL_VERSIONinscene/init. -
The whole contract is
RISE_SDK_VERSION. It moves when anything in this document changes: the patch number for wording only, the minor number for an addition, the major number for a break (§1). -
The MCP layer follows MCP's own versions (
PROTOCOL_VERSIONS) and the Apps extension's (PROTOCOL_VERSION). They change when MCP changes, not when this contract does.
An embedder imports the contract's values from src/core/rise-sdk.js, which holds no logic, and validates with validateRiseCurrent from src/core/rise-current.js. RISE does not yet publish these as a package; they live in this repository.
- No pixels. The same Current may look different across releases, hosts and screens. Engine rendering, layout, scrims, transitions and the exact text sizes are RISE's to change. Only the names, values and behaviours above are the contract.
- No moderation. A scene can draw anything within its budget. The words and pictures a model composes are the host model's responsibility, as its text is.
-
ChatGPT is unmeasured. Whether ChatGPT's sandbox allows
blob:workers andui/update-model-contexthas not been measured (CC-001, pending). Until it is, generated scenes are promised for Claude only, and native scenes everywhere. -
Figures in a host are unmeasured. A figure is an image of a
blob:URL. Whether a host's sandbox lets the card show ablob:image has not been measured in Claude or ChatGPT; where it does not, the figure fails as an image and the reading's own field shows instead. -
The internal score is not the contract. How a Current lowers to RISE's Experience Program and Session (
compileRiseCurrent) may change without a version bump. - Interactive scenes are taught in RISE Live only (§7.11). No host's model is told of them yet, so in a host's card they appear only if a model writes one unprompted.
- Not in this version: an imagery lookup a model can call, fonts inside the scene worker, WebGPU, instant cue replay on seek, and Dive or realtime Live.
src/core/rise-sdk.test.js fails when:
- a value in any table headed Constant | Value differs from the constant it names, or names a constant the contract does not export;
- a key of
BEAT_LIMITS,RISE_CURRENT_LIMITS,SCENE_LIMITSorLIBRARY_DEFAULTShas no row here, or an export ofsrc/core/rise-sdk.jsis not named here; - the field tables differ from the tool's input schema, or the refusal codes from the validators' own;
- the style, face, engine or parameter tables differ from
STYLES,TYPE_FACESorSCENE_MANIFESTS, or the engines differ from whatdescribeManifests()tells the model; - the
rise.libtable differs from the keys ofcreateSceneLibrary(…), the message table fromTO_WORKERandTO_HOST, or the banned lists fromSHADOWED_GLOBALSandSTATIC_ONLY_NAMES; - the example module, the refusal lines or the report lines differ from what the guide, the server and the card produce, or the figure's element list from
SVG_ELEMENTS; -
src/core/rise-sdk.jsexports a function, or imports anything outside the core and the scenes.
Published from docs/specs/RISE-SDK.md. Edits made here are overwritten on the next push to main.
Start here
- COMPOSER-FIRST-ROADMAP
- VISION
- PROJECT-KNOWLEDGE
- ARCHITECTURE
- SYSTEM-DESIGN-REVIEW-2026-08-22
- ACCOUNT-SAVES
- RELEASING
- PLUS-CUSTOMER-LIFECYCLE
- AGENTS
Release
Pilot
- LIVE-CURRENT
- LIVE-HANDOFF
- LIVE-MCP
- DIRECTORY-SCREENSHOTS
- INSPECTOR-RUN-2026-10-10
- CLAUDE-RUN-2026-10-10
- CREATIVE-CONTROL-EVALS
- PERCEPTION-EVAL
- SOUND-LEVELS
- WORDING
- RUN-2026-10-08
- COMPOSER-INSTRUMENTS
- CHATGPT-DEMO
- EMBED-WITNESS
- RISE-GATE0
- CHATGPT-HOST-2026-10-04
- LIVE-002-HOST-2026-10-05
- EMBED-STAGE-HOST-2026-10-07
- RELEASE-GATES-2026-10-07
- EMBED-SAFETY-2026-10-05
- LIVE-GEMINI
- LIVE-EVALUATION
- LIVE-RED-TEAM
- CURRENT-CONSOLIDATION
- RISE-CHANGES
The Archive - texts and their editing
- ARCHIVE-CANON-SPEC
- ARCHIVE-CLEANSING-SPEC
- CORPUS-REVIEWER-PROMPT
- TYPESETTING-CANON
- PHRASE-CHUNKING-STUDY
- CHUNKER-AWARENESS-STUDY
- LIBRARY-SPEC
- SOL-PD-ACQUISITIONS-DOSSIER-LITERATURE-2026-07-28
Authoring - scores, rooms, and rendering
- EXPERIENCE-PROGRAM-SPEC
- LIVE-CURRENT-EVENTS-V1
- RISE-CURRENT-V1-SLICE
- RISE-SDK
- SCRIPTORIUM-SPEC
- WORKSHOP-COMPOSITION-STUDIO-SPEC
- AGENT-COMPOSITION-AND-RENDER-SPEC
- NARRATION-LANE-SPEC
- RECITATION-SPEC
- SCRIPTORIUM-STRENGTHENING-SPEC
Reading surfaces
- PAGE-MODE-SPEC
- RHYTHMIC-VISUAL-PRESENCE-SPEC
- LATERAL-TRAVERSAL-SPEC
- CONTINUOUS-FIELD-SPEC
- JEV-VARIANCE
- INVOCATION-SKINS
- PHASE-2-SAFETY-SPEC
- SPATIAL-CHAMBER-SPEC
- Premium_Mobile_Chamber
Imagery - what may be shown, and on whose authority
- MUSEUM-ATLAS
- SOURCE-CURATION-SPEC
- SOURCE-EXPANSION-SPEC
- ATRIUM-IMAGERY-SPEC
- PERICOPE-IMAGERY-SPEC
- icon-museum-request-draft
- TEXT-ATTUNED-IMAGERY-SPEC
The Chapel
Direction and unbuilt work
Affect and the Emotions room
Reading decision service and retired gate
Product direction and team tracking
- README
- READER-DIRECTION
- CONSOLIDATED-READER
- COMPOSER-FIRST-ROADMAP
- TRACKER-DESIGN
- 2026-10-03-direction-decisions
- 2026-10-04-composer-decision
- 2026-10-05-consolidated-reader-decisions
- 2026-10-05-embed-stage-decision
- 2026-10-06-score-speech-service-roadmap
- 2026-10-07-current-with-a-look
- 2026-10-07-reader-control-and-replay
- 2026-10-05-one-colour-vocabulary-design
- 2026-10-07-chrome-follows-theme
- 2026-10-07-gallery-follows-with-works
- 2026-10-05-canonical-home-design
- 2026-10-04-reader-evidence
- 2026-10-original-performance-roadmap
- 2026-10-08-voice-hardening-2-findings
- 2026-10-09-feelings-sounds-parked
- 2026-10-10-connector-directory-readiness
- 2026-10-10-connector-submission-packet