references/actions.md
# Playback actions field reference
Actions are the ordered playback verbs at `scene.actions`.
They are separate from `scene.content`: content says what the page contains;
actions say what happens over time.
This chapter is derived from the shared `@openmaic/dsl` Action union,
`applyActionEdit`, and the scene validators.
## Root and ordering
```json
"actions": [
{ "id": "a1", "type": "speech", "text": "Welcome." }
]
```
The array is optional on the shared scene type. An absent array and an empty
array both mean there are no playback actions available to run.
Array order is playback order.
All variants include:
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `id` | string | yes | action identity |
| `type` | action-type union | yes | selects the variant |
| `title` | string | no | optional authoring/display metadata |
| `description` | string | no | optional authoring/display metadata |
The pure runtime validator checks the id, known type, and each variant's
required fields. The cross-language JSON Schema is stricter about every
optional field. The app's interactive/PBL write boundary does not currently
run that full JSON Schema, so preserve known fields and do not treat tolerance
of an unknown nested member as authorization to invent it.
## Visual pointer actions
### `spotlight`
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `elementId` | string | yes | slide element to focus |
| `dimOpacity` | number | no | opacity of the dimmed region |
### `laser`
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `elementId` | string | yes | slide element to point at |
| `color` | string | no | laser color |
### `play_video`
`elementId: string` is required and identifies the slide video element.
The type system does not prove that any referenced element exists or is the
right slide element type. Cross-check `scene.content.canvas.elements`.
These actions are slide-oriented. The shared `SLIDE_ONLY_ACTIONS` list is the
runtime's authoritative category; do not attach them to a non-slide scene just
because the generic scene shape accepts an actions array.
## Speech
### `speech`
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `text` | string | yes | narration text |
| `audioId` | asset reference string | no | synthesized narration asset |
| `audioInvalidated` | boolean | no | prevents legacy derived-id fallback after invalidation |
| `voice` | string | no | voice hint/binding field |
| `speed` | number | no | playback/synthesis speed field |
When a generic pointer changes an existing speech action's `text`,
`patch_stage` preserves the retired `set_speech` op's safety behavior and
removes stale `audioId` (and historical `audioUrl`) in that op.
The new line is silent until `generate_tts` synthesizes it. `generate_tts`
remains available.
If one atomic batch deliberately sets new text and a known new audio id, put
the text op first and the audio-id op second. Stale-audio cleanup happens when
the text op is applied.
## Whiteboard lifecycle
### `wb_open`
No variant-specific fields.
### `wb_close`
No variant-specific fields.
### `wb_clear`
No variant-specific fields.
### `wb_delete`
Requires `elementId: string` naming a whiteboard element.
The data contract does not enforce lifecycle order. Opening before drawing,
deleting only existing ids, and closing after work are runtime/procedure
responsibilities.
## Whiteboard drawing
### `wb_draw_text`
Required:
- `content: string`
- `x: number`
- `y: number`
Optional:
- `elementId: string`
- `width: number`
- `height: number`
- `fontSize: number`
- `color: string`
### `wb_draw_shape`
Required:
- `shape: "rectangle" | "circle" | "triangle"`
- `x`, `y`, `width`, `height`: numbers
Optional `elementId` and `fillColor` are strings.
### `wb_draw_chart`
Required:
- `chartType`: `bar` / `column` / `line` / `pie` / `ring` / `area` / `radar` / `scatter`
- `x`, `y`, `width`, `height`: numbers
- `data.labels: string[]`
- `data.legends: string[]`
- `data.series: number[][]`
Optional `elementId: string` and `themeColors: string[]`.
The action type does not verify series/label dimensions.
### `wb_draw_latex`
Required `latex: string`, `x: number`, and `y: number`.
Optional `elementId`, `width`, `height`, and `color`.
The contract does not parse LaTeX.
### `wb_draw_table`
Required `x`, `y`, `width`, `height`, and `data: string[][]`.
Optional `elementId`.
Optional `outline` has required `width:number`, `style:string`, and
`color:string` when the object is present.
Optional `theme` has required `color:string` when present.
The action contract leaves outline `style` open as a string; do not copy the
slide table's closed line-style assumption here without renderer evidence.
### `wb_draw_line`
Required numeric fields:
```text
startX | startY | endX | endY
```
Optional:
- `elementId: string`
- `color: string`
- `width: number`
- `style: "solid" | "dashed"`
- `points`: one of `['','arrow']`, `['arrow','']`,
`['arrow','arrow']`, or `['','']`
### `wb_draw_code`
Required:
- `language: string`
- `code: string`
- `x: number`
- `y: number`
Optional `elementId`, `width`, `height`, and `fileName`.
The language string is open. Syntax support depends on the consumer and is not
validated here.
### `wb_edit_code`
Required:
- `elementId: string`
- `operation`: `insert_after` / `insert_before` / `delete_lines` / `replace_lines`
Optional:
- `lineId: string`
- `lineIds: string[]`
- `content: string`
Which optional fields are required for each operation is not expressed by the
persisted union. Read a working action or the whiteboard runtime before
constructing one.
## Discussion
### `discussion`
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `topic` | string | yes | discussion topic |
| `prompt` | string | no | additional discussion instruction |
| `agentId` | string | no | selected agent identity |
The contract does not prove that `agentId` belongs to the stage roster.
## Widget actions
### `widget_highlight`
Requires `target: string`; optional `content: string`.
### `widget_setState`
Requires `state: Record<string, unknown>`; optional `content: string`.
The state payload is intentionally open because each widget owns its state
shape.
### `widget_annotation`
Requires `target: string`; optional `content: string`.
### `widget_reveal`
Requires `target: string`; optional `content: string`.
The target selector/id language and its existence are widget-owned and are not
validated by the course document.
## Synchronous categories
The shared contract exports three runtime lists:
- `FIRE_AND_FORGET_ACTIONS`
- `SLIDE_ONLY_ACTIONS`
- `SYNC_ACTIONS`
Those lists, not guesses based on names, determine scheduling categories.
This reference does not duplicate their current members because the runtime
values are authoritative and may evolve with the contract package.
## Visible text projection
`read_stage detail:"text"` includes established user-facing strings:
- common action `title` and `description`
- speech `text`
- discussion `topic` and `prompt`
- whiteboard text `content`
- whiteboard code `code` and `fileName`
- widget target/content strings for highlight, annotation, and reveal
- widget set-state `content`, but not its open state JSON
Geometry, ids, answer-like state, audio ids, and colors are source-only.
## Generic pointer behavior
Action paths are rooted at the scene:
```text
/actions/0/text
/actions/1/elementId
/actions/2/dimOpacity
```
An array index is not a stable identity. Find it from fresh source by matching
the action `id`.
`set` replaces a field; `remove` deletes or splices. There is no action-specific
insert op in `patch_stage`. Inserting means setting the complete resulting
`/actions` array with ids chosen by the caller.
The retired `insert_speech` op minted `act-...` ids. The generic pointer
does not. Preserve existing ids and ensure any caller-created id is unique.
## Common rejection reasons
- Path begins `/content/actions` instead of `/actions`.
- Index no longer points to the action id read earlier.
- A required variant field is removed.
- `type` is unknown or changed without supplying its new required fields.
- `set` omits value or `remove` carries value.
- A path crosses a scalar or missing intermediate object.
## Common semantic mistakes not fully caught
- Element-targeting action points to a missing element.
- Speech text changes but TTS is not regenerated.
- A whiteboard edit runs before its draw/open action.
- Duplicate action ids are introduced in a whole-array rewrite.
- Widget target names do not exist in widget state/DOM.
- Discussion `agentId` is not in the course roster.
- Array reordering changes narration timing unintentionally.
## Worked example 1: reword speech safely
Read source:
```json
read_stage({
"path": "/scenes/1",
"detail": "source"
})
```
Locate the speech by id and note its index:
```text
/actions/2 = { "id":"act-intro", "type":"speech", ... }
```
Patch:
```json
patch_stage({
"target": "/scenes/1",
"intent": "Make the opening narration more direct",
"ops": [
{
"op": "set",
"path": "/actions/2/text",
"value": "We will test this idea with one concrete example."
}
]
})
```
Read source again. Verify the new text and the absence of the old `audioId`.
Then call `generate_tts` for this scene and read back once more.
## Worked example 2: retarget a spotlight
Read the slide source and verify both ids:
```text
/content/canvas/elements/4/id = "el-result"
/actions/3 = { "id":"act-focus", "type":"spotlight", ... }
```
Patch only the reference:
```json
patch_stage({
"target": "/scenes/scene_slide",
"intent": "Move the spotlight to the result label",
"ops": [
{
"op": "set",
"path": "/actions/3/elementId",
"value": "el-result"
}
]
})
```
Read source again and confirm no action ordering changed.
## Worked example 3: remove an optional discussion prompt
Read `/scenes/4/actions` with `detail:"source"` and locate the discussion index.
```json
patch_stage({
"target": "/scenes/4",
"intent": "Let the discussion topic stand without an extra prompt",
"ops": [
{
"op": "remove",
"path": "/actions/1/prompt"
}
]
})
```
Read `/scenes/4/actions` again and verify `topic`, `id`, and position remain.
## Hard rules
- Locate actions by id in fresh source, then use the current array index.
- Preserve ids when editing or reordering.
- Re-synthesize narration after speech text changes.
- Cross-check every elementId/agentId/target against its owning structure.
- Write complete arrays only when a leaf cannot express the change.
- Read back after every write.
references/pbl.md
# PBL projectV2 field reference
This chapter describes the current PBL payload at
`scene.content.projectV2`.
It is derived from the app's PBL v2 types, `applyPblEdit`, the shared DSL PBL
contract, and `validateAppScene`.
Authoring paths: `generate_scene` with `type:"pbl"` mints a complete
`projectV2` from the page brief (v2 single-call planner) — this is the normal
way a PBL page is created; `patch_stage` then fine-tunes the project's
authoring fields, and `edit_deck` inserts a blank pbl stub when the page must
be created empty first.
PBL has the widest gap between authoring types and the persisted write
barrier. Read the validation section before patching.
## Content root
```json
{
"type": "pbl",
"projectV2": { "...": "..." }
}
```
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `type` | string | yes | exactly `pbl`; must match `scene.type` |
| `projectV2` | object | no for legacy compatibility | current PBL project |
| `projectConfig` | object | no | read-only legacy v1 data |
`patch_stage` closes this content root. Unknown siblings of `projectV2` and
`projectConfig` are rejected.
Do not author new `projectConfig` data. It is retained for old scenes and the
current runtime uses `projectV2`.
## Validation boundary
For a new v2-only scene, `validateAppScene` requires `projectV2` to contain
array containers for:
```text
milestones | roles | threads
```
The shared PBL guard additionally recognizes the packaged project skeleton,
but deliberately tolerates unknown project-tree members because historical
documents carry app runtime fields.
When a scene contains a sound, non-empty legacy `projectConfig`, the app write
barrier preserves a damaged `projectV2` as inert historical bytes rather than
blocking every aggregate save.
Therefore PBL validation is not a closed field-by-field schema. The TypeScript
interfaces below are the authoring reference, but an incomplete or misspelled
nested field may be accepted. Always read back and, for risky changes, exercise
the PBL surface.
## Project root
Core fields in `PBLProjectV2`:
| Field | Type | Required | Legal values / meaning |
| --- | --- | --- | --- |
| `uiPhase` | string union | yes | `hero` / `generating` / `workspace` / `completed` |
| `title` | string | yes | project title |
| `description` | string | yes | what the learner will build/do |
| `learningObjective` | string | no | what the learner should learn |
| `gains` | `string[]` | no | learner-facing takeaways |
| `proficiency` | string union | yes | `""` / `beginner` / `intermediate` / `advanced` |
| `language` | string | yes | locale fallback |
| `languageDirective` | string | no | authoritative nuanced content-language policy |
| `tags` | `string[]` | yes | free-form tags |
| `schemaVersion` | number | no | reserved packaged-format version |
| `status` | string union | yes | `designing` / `review` / `active` / `completed` / `archived` |
| `roles` | `PBLRole[]` | yes | agent participant records |
| `milestones` | `PBLMilestone[]` | yes | ordered stages |
| `submissions` | `PBLSubmission[]` | yes | learner deliverables/runtime data |
| `evaluations` | `PBLEvaluation[]` | yes | feedback/runtime data |
| `threads` | `PBLAgentThread[]` | yes | agent chat/runtime data |
| `engagementEvents` | `PBLEngagementEvent[]` | yes | runtime analytics ledger |
| `createdAt` | string | yes | ISO timestamp by contract documentation |
| `updatedAt` | string | yes | ISO timestamp by contract documentation |
Optional runtime/adaptive fields include:
- `proficiencyAssessment`
- `runtimeEvents`
- `runtimeResetEpoch`
- `pendingHandover`
- `pendingTaskCompletion`
- `pendingOpenTaskPriorQuizResults`
Those fields belong to learner runtime state, not ordinary course authoring.
Do not reset, synthesize, or “clean up” them through content editing.
Unlike the old `applyPblEdit` menu, generic pointer writes do not automatically
refresh `projectV2.updatedAt`. If the product requires that timestamp for an
authoring change, include an explicit second op with a real ISO timestamp. Do
not rewrite it merely for cosmetic diff consistency without a consumer need.
## Roles
A `PBLRole` is:
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `id` | string | yes | role identity |
| `type` | role union | yes | see below |
| `name` | string | yes | displayed name |
| `description` | string | no | short learner-facing avatar tooltip |
| `systemPrompt` | string | no | internal persona/behavior prompt; not learner-facing |
Role type union:
```text
user | instructor | evaluator | mentor | collaborator | simulator | system
```
The current product creates one Instructor role. `simulator` and `system` are
scenario message role types, not ordinary `roles[]` records according to the
type documentation.
`description` must not expose internal mechanics. `systemPrompt` is internal
and is excluded from the visible-text projection.
Threads reference roles through `agentId`. Changing a role id requires updating
every matching thread and any other reference atomically. Prefer preserving ids.
## Milestones
A milestone requires:
| Field | Type | Meaning |
| --- | --- | --- |
| `id` | string | identity |
| `title` | string | displayed stage title |
| `status` | `locked` / `active` / `completed` | lifecycle state |
| `order` | number | 1-based authored ordering convention |
| `microtasks` | `PBLMicrotask[]` | ordered steps |
Optional authoring fields:
| Field | Type | Meaning |
| --- | --- | --- |
| `description` | string | stage description |
| `documents` | `PBLDocument[]` | legacy/future resource slot |
| `briefing` | string | Instructor setup script |
| `completionCriteria` | string | stage completion rule |
| `debrief` | string | wrap-up script |
| `synthesisCheck` | `{coreConcept:string}` | one-time integrative check |
| `scenarioStage` | union | `prep` / `roleplay` / `wrapup` |
Optional `internalAssessment` is runtime-owned teaching state.
The old `applyPblEdit add_milestone` minted an `ms-...` id, set status `locked`,
set `order` to length + 1, and created an empty `microtasks` array. A generic
pointer does none of that automatically. Adding/reordering means supplying the
complete resulting array and maintaining ids/order/status yourself.
## Microtasks
Required fields:
| Field | Type | Legal values / meaning |
| --- | --- | --- |
| `id` | string | task identity |
| `title` | string | task title |
| `status` | union | `todo` / `in_progress` / `completed` / `skipped` |
| `assignee` | literal | exactly `user` in the current product |
| `hints` | `string[]` | learner help |
| `order` | number | 1-based authored ordering convention |
Optional authoring fields:
- `description: string`
- `completionCriteria: string`
- `successWhen: string`
- `characterObjective: string`
- `skillFocus: string`
- `narration: string`
- `learnerBrief: string`
Optional runtime fields:
- `internalAssessment`
- `completionReason`
- `engagement`
`successWhen` is a hidden, concrete observable advance criterion in scenario
projects. It is not learner-facing and is excluded from text search.
`characterObjective` is private character motivation. It must not be narrated,
evaluated as learner copy, or exposed in `learnerBrief`.
`learnerBrief` is pure display guidance and may fall back to `description` when
absent. It should orient without revealing `successWhen` or private character
facts.
The old `applyPblEdit add_microtask` minted `mt-...`, set `status:"todo"`,
`assignee:"user"`, empty hints, and order length + 1. Generic pointer writes do
not mint or normalize those fields.
## PBL documents
A milestone document is:
```json
{
"id": "doc-1",
"title": "Reference",
"content": "...",
"docType": "markdown"
}
```
`docType` legal values:
```text
markdown | reference | starter_file
```
The current generators do not author this legacy/future slot according to the
type comment. Treat inherited documents conservatively.
## Scenario package
Presence of `projectV2.scenario` marks a role-play scenario project.
Required:
- `setting: string`
- `characters: PBLScenarioCharacter[]`
Optional:
- `sceneVisual`
- `goal: string`
- `rules: string`
- `learnerRole: string`
A character requires `id`, `name`, and `persona`. Optional fields are
`situation`, `boundaries`, `avatar`, and `openingLine`.
`situation` is the character's concrete current circumstance and is distinct
from stable `persona`.
`boundaries` are hard safety limits.
Optional `sceneVisual` fields are `caption`, `bg1`, `bg2`, `accent`, and
`motifs:string[]`. The type comments describe colors as hex examples, but the
TypeScript fields are strings and rendering sanitizes malformed values. Do not
claim the write validator enforces hex.
## Runtime-owned arrays
### `submissions`
Learner work. A submission includes identity, task reference, kind, content,
and created time, with optional file metadata/summary.
Do not author or delete learner submissions while editing course design.
### `evaluations`
Instructor/evaluator feedback, scores, stars, and possible scenario act-goal
review. Runtime-owned.
### `threads`
Each thread has `agentId`, messages, and optional earlier summary. Messages are
conversation state. Do not use course editing to seed or rewrite learner chat.
### `engagementEvents` and `runtimeEvents`
Append-only ledgers. They are not content arrays and must not be reordered or
trimmed by an authoring patch.
### pending gates
`pendingHandover` and `pendingTaskCompletion` encode learner progress gates.
Changing them bypasses runtime operations and is out of scope for content
authoring.
## Visible text projection
`read_stage detail:"text"` includes:
- project title, description, learningObjective, and gains
- role name and learner-facing role description
- milestone title, description, briefing, and debrief
- microtask title, description, learnerBrief, and hints
- visible action text
It intentionally excludes:
- role `systemPrompt`
- `successWhen`
- `characterObjective`
- assessments, submissions, evaluations, threads, ledgers, and pending gates
Use `grep_stage scope:"source"` only when you intentionally need an internal
field. Source search can expose runtime/private data; do not echo it to learners.
## Pointer/application-layer impedance
The old PBL menu had semantic operations:
- `set_project`
- `set_role`
- `set_milestone`
- `set_microtask`
- add/delete milestone
- add/delete microtask
Those operations found records by id, minted ids for additions, renumbered
orders after deletion, and refreshed `updatedAt`.
`patch_stage` intentionally uses raw pointers instead. It addresses arrays by
current index and performs none of those PBL-specific repairs. This is the main
apply-layer impedance exposed by the spike.
Consequences:
- Fresh source is mandatory before every index write.
- Array additions/deletions require complete-array replacement.
- The caller owns id uniqueness, order normalization, statuses, thread seats,
and updatedAt.
- The current validator proves containers, not every cross-reference.
## Common rejection reasons
- Pointer starts `/projectV2/...` instead of `/content/projectV2/...`.
- Intermediate `projectV2`, milestone, or task does not exist.
- Index is stale or out of bounds.
- Root content gains an unknown field.
- Core v2 containers are removed or cease to be arrays.
- `projectConfig` becomes a primitive.
- Scene/content discriminators no longer agree.
## Common semantic errors that may still persist
- Duplicate role/milestone/microtask ids.
- Orders have gaps or disagree with array order.
- First milestone remains locked when a new course expected it active.
- A role id changes but its thread `agentId` does not.
- Scenario fields leak private goals into learner-facing copy.
- Design edits overwrite submissions, evaluation, chat, or progress.
- A nested field is misspelled under the intentionally open historical project
tree and the renderer silently ignores it.
- `updatedAt` is stale after a generic pointer write.
## Worked example 1: edit one milestone title
Read source:
```json
read_stage({
"path": "/scenes/4",
"detail": "source"
})
```
Locate by id, then note the current index:
```text
/content/projectV2/milestones/1/id = "ms-research"
/content/projectV2/milestones/1/title = "Research"
```
Patch the leaf:
```json
patch_stage({
"target": "/scenes/4",
"intent": "Make the research milestone outcome explicit",
"ops": [
{
"op": "set",
"path": "/content/projectV2/milestones/1/title",
"value": "Research and choose one bridge design"
}
]
})
```
Read source again. Confirm the same milestone id, status, order, and microtasks.
## Worked example 2: update learner brief without exposing hidden success
Read source and compare:
```text
/content/projectV2/milestones/1/microtasks/0/learnerBrief
/content/projectV2/milestones/1/microtasks/0/successWhen
```
Patch only the visible brief:
```json
patch_stage({
"target": "/scenes/scene_pbl",
"intent": "Clarify the learner-facing task orientation",
"ops": [
{
"op": "set",
"path": "/content/projectV2/milestones/1/microtasks/0/learnerBrief",
"value": "Compare the two designs and explain which trade-off matters most."
}
]
})
```
Read source and text. Verify `successWhen` is byte-identical and the new brief
appears in the visible projection.
## Worked example 3: change a role description, not its internal prompt
Read source and identify the role by id:
```text
/content/projectV2/roles/0/id = "role-instructor"
/content/projectV2/roles/0/description = "..."
/content/projectV2/roles/0/systemPrompt = "..."
```
Patch:
```json
patch_stage({
"target": "/scenes/4",
"intent": "Make the Instructor tooltip more reassuring",
"ops": [
{
"op": "set",
"path": "/content/projectV2/roles/0/description",
"value": "I will help you break the project into manageable decisions."
}
]
})
```
Read back. Confirm id, type, name, and `systemPrompt` are unchanged.
## Hard rules
- Treat project design and learner runtime state as separate ownership domains.
- Never edit submissions, evaluations, threads, ledgers, or pending gates as a
course-authoring convenience.
- Find records by id in source, then patch the current index.
- Preserve ids and cross-references.
- Whole-array writes own normalization that the old semantic apply ops supplied.
- Do not rely on the tolerant PBL validator to catch nested misspellings.
- Read source and visible text back after every write.
references/quiz.md
# Quiz content field reference
This chapter describes the persisted quiz structure at `scene.content`.
It is derived from `@openmaic/dsl`'s `QuizContent`, `QuizQuestion`, and
`QuizOption` contracts, the quiz editor operations, and the document write
validator. It states only behavior those sources establish.
## Root
```json
{
"type": "quiz",
"questions": []
}
```
| Field | Type | Required | Legal values / meaning |
| --- | --- | --- | --- |
| `type` | string | yes | exactly `"quiz"`; must agree with `scene.type` |
| `questions` | `QuizQuestion[]` | yes | ordered question list |
The content root is closed for `patch_stage`: fields other than `type` and
`questions` are rejected.
The page title is `scene.title`, outside content. Change it with `edit_deck`,
not `patch_stage`.
## QuizQuestion
```json
{
"id": "q1",
"type": "single",
"question": "Which value is prime?",
"options": [
{ "label": "4", "value": "A" },
{ "label": "5", "value": "B" }
],
"answer": ["B"],
"analysis": "5 has no positive divisors other than 1 and itself.",
"points": 1
}
```
| Field | Type | Required | Legal values / semantics |
| --- | --- | --- | --- |
| `id` | string | yes | stable question identity used by editor operations |
| `type` | string union | yes | `single` / `multiple` / `short_answer` |
| `question` | string | yes | learner-facing question stem |
| `options` | `QuizOption[]` | no | choice rows; normally used by single/multiple |
| `answer` | `string[]` | no | correct option values, or accepted short-answer values |
| `analysis` | string | no | answer explanation shown by quiz surfaces that expose analysis |
| `commentPrompt` | string | no | optional prompt field; exact rendering timing is not specified here |
| `hasAnswer` | boolean | no | signals whether a short-answer item has a supplied answer |
| `points` | number | no | score weight/value |
The question object is closed for `patch_stage`. A misspelling such as
`analaysis` is rejected rather than stored.
The runtime contract requires `id`, `type`, and `question` by type. The
`patch_stage` quiz check also verifies those fields are strings and that
`type` is one of the three values above.
No minimum or maximum string length is specified by the persisted type.
No positive-only or integer-only constraint is specified for `points` in the
type. Do not invent one.
## Question types
### `single`
The editor models the answer as `string[]` even when one option is correct.
The choice editor's `toggleCorrect` operation keeps single-choice behavior by
selecting one row. A generic pointer bypasses that menu behavior: when writing
`answer` directly, supply the complete intended array.
Example:
```json
"answer": ["B"]
```
### `multiple`
Multiple option values may appear in `answer`.
Example:
```json
"answer": ["A", "C"]
```
The persisted type does not require answer order to match option order, but
keeping it aligned makes diffs and review easier.
### `short_answer`
Choice `options` are optional and normally absent.
The grading code treats a short-answer question without `hasAnswer` as not
auto-gradable. This is an established consumer behavior, not a schema rule.
`answer` remains a string array when present.
## QuizOption
```json
{ "label": "5", "value": "B" }
```
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `label` | string | yes | learner-facing option text |
| `value` | string | yes | stable value stored in the question's `answer` array |
The option object is closed. Unknown fields and wrong types are rejected.
`label` and `value` are different. Changing a label preserves correctness only
when the value is unchanged. Changing a value requires updating every matching
entry in `answer` in the same atomic batch.
## Ordering and identity
`questions` array order is display order.
`options` array order is the displayed choice order.
Generic pointer writes address zero-based array indices. Read the source again
immediately before an index-based edit; an earlier insert or reorder changes
the index.
`patch_stage` has no quiz-specific add menu and does not mint quiz identities.
Adding a question or option means setting the complete resulting array,
including valid ids and option values supplied by the caller.
The pointer implementation accepts only canonical, already-existing array
indices. JSON Patch's conventional `/-` append token is **not supported**, and
an index equal to the current length is out of bounds. Read the current array,
append in memory, then `set` the array field to the complete result.
The existing runtime validator does not define an id format or global
uniqueness rule for quiz question ids. Preserve existing ids. For new ids,
follow the neighboring document's convention and ensure uniqueness yourself.
## Answer coupling
The important invariant is referential:
```text
question.answer[] value -> one question.options[].value
```
The TypeScript type permits values that do not point to an option. The generic
write validation does not prove this relationship. A structurally accepted
quiz may therefore still have an ungradeable or always-wrong answer.
When deleting an option, remove its value from `answer` in the same call.
When reassigning option values, update `answer` in the same call.
When changing `type`, inspect `options`, `answer`, and `hasAnswer` together.
## Visible text projection
`read_stage detail:"text"` includes:
- `question`
- every option `label`
- `analysis` when present
- `commentPrompt` when present
- visible action text attached to the scene
It does not treat `answer` values as learner-facing text.
`grep_stage scope:"text"` searches that same projection.
Use `scope:"source"` when looking for an option value, question id, field name,
or answer key.
## Common rejection reasons
- Path starts `/questions/...` instead of `/content/questions/...`.
- Question or option index is stale.
- An intermediate array/object does not exist.
- `remove` targets a missing optional field.
- A required field (`id`, `type`, `question`, root `questions`) is removed.
- A question type is not `single`, `multiple`, or `short_answer`.
- An option lacks a string `label` or `value`.
- An unknown question/option/content-root field is introduced.
## Common semantic mistakes that validation does not catch
- Correct answer values no longer exist in `options`.
- A `single` question has multiple answer values after a whole-array write.
- A `short_answer` question is expected to auto-grade but lacks `hasAnswer`.
- Duplicate question ids are introduced in a rewritten array.
- Option labels move but answer values are unintentionally regenerated.
- `points` is technically a number but unsuitable for the scoring policy.
## Worked example 1: change one option label
Read the source:
```json
read_stage({
"path": "/scenes/2",
"detail": "source"
})
```
Locate the exact option:
```text
/content/questions/0/options/1
{ "label": "5", "value": "B" }
```
Patch only its label:
```json
patch_stage({
"target": "/scenes/2",
"intent": "Clarify the second answer choice",
"ops": [
{
"op": "set",
"path": "/content/questions/0/options/1/label",
"value": "5(质数)"
}
]
})
```
Read back:
```json
read_stage({
"path": "/scenes/2",
"detail": "source"
})
```
Verify that the label changed and `value:"B"` plus `answer:["B"]` did not.
## Worked example 2: change an option value without breaking the key
Read source and locate:
```text
/content/questions/0/options/1/value = "B"
/content/questions/0/answer = ["B"]
```
Write both coupled fields atomically:
```json
patch_stage({
"target": "/scenes/scene_quiz",
"intent": "Rename the second option value while preserving correctness",
"ops": [
{
"op": "set",
"path": "/content/questions/0/options/1/value",
"value": "prime"
},
{
"op": "set",
"path": "/content/questions/0/answer",
"value": ["prime"]
}
]
})
```
Read back source and confirm both writes landed together.
## Worked example 3: remove optional analysis
Read source first and prove `analysis` exists:
```json
read_stage({
"path": "/scenes/2",
"detail": "source"
})
```
Remove the leaf:
```json
patch_stage({
"target": "/scenes/2",
"intent": "Remove the outdated answer explanation",
"ops": [
{
"op": "remove",
"path": "/content/questions/0/analysis"
}
]
})
```
Read source again, then optionally run:
```json
grep_stage({
"query": "outdated phrase",
"scope": "text"
})
```
The source must lack `analysis`, and the old visible phrase must have no hit.
## Worked example 4: add one question
After reading `/scenes/2` with `detail:"source"`, preserve every existing
question exactly and append the new complete object in the value written to the
array itself:
```json
patch_stage({
"target": "/scenes/2",
"intent": "Add a second quiz question",
"ops": [
{
"op": "set",
"path": "/content/questions",
"value": [
{
"id": "q1",
"type": "single",
"question": "Which value is prime?",
"options": [
{ "label": "4", "value": "A" },
{ "label": "5", "value": "B" }
],
"answer": ["B"]
},
{
"id": "q2",
"type": "short_answer",
"question": "Name the smallest prime number.",
"answer": ["2"],
"hasAnswer": true
}
]
}
]
})
```
Do not use `/content/questions/-`; it is rejected as a non-canonical array
index.
## Worked example 5: add one option
Read the complete current option array, append a new `{label,value}` pair, and
set `/content/questions/0/options` to that complete resulting array. If the new
option is correct, set `/content/questions/0/answer` in the same atomic batch.
Never write `/content/questions/0/options/-`.
## Hard rules
- Read source, never tree, to obtain indices and full neighboring state.
- Patch the smallest leaf unless coupled fields must change atomically.
- Preserve question ids and option values unless the intent explicitly changes them.
- Treat `answer` and option `value` as one invariant.
- Use complete arrays for additions/reorders; generic pointers do not mint quiz ids.
- Read back after every write.
references/widget.md
# Interactive and widget field reference
This chapter describes `scene.content` when `scene.type` is `interactive`.
The fields come from the shared interactive contract, the app's six
`WidgetConfig` variants, `applyWidgetEdit`, and `validateAppScene`.
## Interactive content root
```json
{
"type": "interactive",
"html": "<!doctype html>...",
"widgetType": "simulation",
"widgetConfig": {
"type": "simulation"
}
}
```
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `type` | string | yes | exactly `interactive`; must match `scene.type` |
| `html` | string | conditionally | complete HTML document rendered with iframe `srcDoc` |
| `url` | string | conditionally | iframe `src` fallback used when `html` is absent |
| `widgetType` | widget-type union | no | historical/top-level widget discriminator |
| `widgetConfig` | object | no | typed Ultra-mode widget configuration |
At least one of `html` or `url` must be present as a string. The empty string is
still a string and is accepted for historical documents.
When both are present, the contract documents `html` as the `srcDoc` source and
`url` as the fallback when HTML is absent.
`patch_stage` closes the content root to the five fields above. The existing
document validator intentionally remains tolerant inside `widgetConfig` for
historical stored shapes. Type declarations below are therefore stronger than
the current runtime write barrier below that root.
## Widget discriminator
Legal widget types are:
```text
simulation
diagram
code
game
visualization3d
procedural-skill
```
The shared `WidgetConfigBase` requires `type` but permits app-defined extension
fields. The app TypeScript union supplies the field sets below.
`applyWidgetEdit` preserves the existing config type when merging `set_config`:
it chooses `widgetConfig.type`, falling back to `widgetType`. A raw pointer can
write either discriminator independently. Keep all present discriminators
consistent yourself.
## `simulation`
Required fields in the app type:
| Field | Type | Meaning |
| --- | --- | --- |
| `type` | `"simulation"` | discriminator |
| `concept` | string | concept being simulated |
| `description` | string | learner-facing explanation |
| `variables` | `SimulationVariable[]` | adjustable numeric inputs |
A simulation variable is:
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `name` | string | yes | state key |
| `label` | string | yes | learner-facing label |
| `min` | number | yes | lower bound |
| `max` | number | yes | upper bound |
| `default` | number | yes | initial value |
| `unit` | string | no | displayed unit |
| `step` | number | no | control increment |
Optional `presets` is an array of:
```json
{
"name": "Heavy object",
"variables": { "mass": 10 }
}
```
The type does not state that default lies between min and max, that step is
positive, or that every preset key names a declared variable. Those are
semantic responsibilities.
## `diagram`
| Field | Type | Required | Legal values / meaning |
| --- | --- | --- | --- |
| `type` | `"diagram"` | yes | discriminator |
| `diagramType` | string union | yes | `flowchart` / `mindmap` / `hierarchy` / `system` |
| `description` | string | yes | learner-facing explanation |
| `nodes` | `DiagramNode[]` | yes | diagram vertices |
| `edges` | `DiagramEdge[]` | yes | directed connections |
| `revealOrder` | `string[]` | no | node ids in reveal sequence |
A node:
| Field | Type | Required | Values |
| --- | --- | --- | --- |
| `id` | string | yes | node identity |
| `label` | string | yes | visible label |
| `position` | `{x:number,y:number}` | no | explicit position |
| `details` | string | no | extra description |
| `type` | string union | no | `default` / `decision` / `start` / `end` |
An edge requires string `id`, `from`, and `to`; `label` is optional.
The type does not prove that edge endpoints or reveal ids exist in `nodes`.
## `code`
| Field | Type | Required | Legal values / meaning |
| --- | --- | --- | --- |
| `type` | `"code"` | yes | discriminator |
| `language` | string union | yes | `python` / `javascript` / `typescript` / `java` / `cpp` |
| `description` | string | yes | task explanation |
| `starterCode` | string | yes | learner's initial program |
| `testCases` | `CodeTestCase[]` | yes | evaluator cases |
| `hints` | `string[]` | yes | learner help |
| `solution` | string | yes | reference solution |
A test case requires string `id`, `input`, and `expected`. Optional fields are
string `description` and boolean `isHidden`.
`solution`, hidden expected results, and hidden tests are source data, not
visible-text search content. Use `grep_stage scope:"source"` to find them.
The current write barrier does not enforce that test-case ids are unique or
that code parses in the selected language.
## `game`
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `type` | `"game"` | yes | discriminator |
| `gameType` | string union | yes | `quiz` / `puzzle` / `strategy` / `card` |
| `description` | string | yes | learner-facing game description |
| `questions` | `GameQuestion[]` | no | quiz-like game content |
| `scoring` | object | yes | scoring controls |
| `achievements` | object array | no | named unlock conditions |
A game question requires `id`, `question`, `type`, `options`, and `correct`.
Question `type` is `single` or `multiple`.
`options` is `string[]`.
`correct` is a number or number array. The type does not state whether the
number is zero-based, one-based, or an option id. Do not guess: read a working
neighboring game and preserve its convention.
Optional question fields are string `explanation` and number `points`.
`scoring.correctPoints` is required. Optional numeric fields are `speedBonus`,
`comboMultiplier`, and `penalty`.
An achievement requires string `id`, `name`, `description`, `icon`, and
`condition`. The meaning/language of `condition` is not specified by the type.
## `visualization3d`
Required root fields:
| Field | Type | Values |
| --- | --- | --- |
| `type` | literal | `visualization3d` |
| `visualizationType` | union | `molecular` / `solar` / `anatomy` / `geometry` / `physics` / `custom` |
| `description` | string | learner-facing description |
| `objects` | object array | scene objects |
Each object requires `id` and a type from:
```text
sphere | box | cylinder | cone | torus | plane | custom
```
Optional object fields:
- `name: string`
- `position: {x,y,z}`
- `rotation: {x,y,z}`
- `scale: number | {x,y,z}`
- `children: Visualization3DObject[]`
- `material`
- `animation`
Material `type` is `basic`, `lambert`, `phong`, `standard`, or `emissive`.
Optional material fields are `color`, `emissive`, `wireframe`, `transparent`,
and `opacity` with their obvious string/boolean/number types. Numeric ranges
are not specified.
Animation `type` is `orbit`, `rotate`, `bounce`, or `pulse`; optional `speed`
is a number and optional `axis` is `x`, `y`, or `z`.
Optional root `interactions` use a type from:
```text
orbit | zoom | pan | slider | button | toggle
```
They may carry `target`, `label`, `param`, and numeric `min`, `max`, `default`,
`step`.
Optional `camera` has numeric-vector `position`, `target`, and number `fov`.
Optional `lighting` has ambient, directional, and point lights. Color is a
string; intensity is a number; positioned lights may carry `{x,y,z}`.
Optional presets carry string `name`, optional string `description`, and open
`state: Record<string, unknown>`.
Renderer interpretation, units, coordinate handedness, and supported custom
object payload are not specified by these types. Read renderer code before
inventing values outside a working example.
## `procedural-skill`
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `type` | `"procedural-skill"` | yes | discriminator |
| `task` | string | yes | overall learner task |
| `description` | string | yes | task explanation |
| `tools` | `string[]` | no | named tools/resources |
| `steps` | `ProceduralSkillStep[]` | yes | ordered procedure |
| `successCriteria` | `string[]` | no | overall completion criteria |
A step requires string `id`, `title`, and `description`. Optional fields are
`tools: string[]` and `successCriteria: string[]`.
The type does not enforce that step tool names appear in root `tools`, that ids
are unique, or that success criteria are machine-checkable.
## HTML branch
`html` is a complete document string, not the slide rich-text dialect.
The shared contract says it is rendered through iframe `srcDoc`.
`patch_stage` stores the string supplied after structural validation. It does
not call `applyHtmlEdits`; generic pointer `set` replaces the exact string.
`read_stage detail:"text"` removes script/style blocks and tags, collapses
whitespace, and searches the remaining text. It is a projection, not a browser
render or sanitizer.
Security policy, iframe sandbox flags, CSP, script execution, and network
access are not established by the data types. Do not infer them from this
chapter.
## Large HTML and long text: use str_replace, not whole-field set
A 27 KB interactive document should not be rewritten with `set`: repeating
the whole string is expensive, and any transcription error silently corrupts
the page. For one targeted change, replace only the exact anchor inside the
stored string.
1. Read `detail:"source"` and locate the exact snippet in `/content/html`.
2. `patch_stage` with `op:"str_replace"` and a short unique anchor.
3. Read `detail:"source"` again, then `grep_stage` to verify the change and
that no residue remains.
Example: slow a gravity simulation by editing one constant inside a 27 KB
document:
```json
read_stage({
"path": "/scenes/3",
"detail": "source"
})
```
Locate in the source:
```text
/content/html contains "const speed = 0.015 * dt"
```
Patch the one occurrence:
```json
patch_stage({
"target": "/scenes/3",
"intent": "Slow the gravity simulation",
"ops": [
{
"op": "str_replace",
"path": "/content/html",
"oldText": "const speed = 0.015 * dt",
"newText": "const speed = 0.006 * dt"
}
]
})
```
Verify by reading `detail:"source"` again, then `grep_stage` for `0.015`
(expect no hit) and `0.006` (expect one hit).
The anchor must occur exactly once in the stored string. On zero hits the
patch is rejected with the count; on several hits, extend the anchor or set
`replaceAll:true`. `newText` may be the empty string to delete the anchor.
Neither `oldText` nor `newText` may contain a read-side media omission
placeholder. Use `set` only when the whole string genuinely changes.
## Common pitfalls
- `widgetType` and `widgetConfig.type` disagree.
- A pointer begins `/widgetConfig/...` instead of `/content/widgetConfig/...`.
- A config type is changed without replacing the variant-specific fields.
- A simulation default lies outside its range.
- Diagram edges point at missing node ids.
- Game `correct` uses a guessed indexing convention.
- Hidden code-test data is expected to appear in text search.
- A whole config object is rewritten and drops unknown historical fields.
- TypeScript says a field is required, but the historical runtime validator is
permissive below `widgetConfig`; an accepted incomplete config then fails in
a renderer. Acceptance is not proof of semantic completeness.
## Worked example 1: change simulation guidance
Read source:
```json
read_stage({
"path": "/scenes/scene_widget",
"detail": "source"
})
```
Locate:
```text
/content/widgetConfig/type = "simulation"
/content/widgetConfig/description = "Change the mass"
```
Patch the leaf:
```json
patch_stage({
"target": "/scenes/scene_widget",
"intent": "Clarify how to operate the simulation",
"ops": [
{
"op": "set",
"path": "/content/widgetConfig/description",
"value": "Drag the mass slider and compare the acceleration."
}
]
})
```
Read source again and verify that `type`, `variables`, and presets are unchanged.
## Worked example 2: adjust one simulation bound
Read source and identify the variable index by its `name`:
```text
/content/widgetConfig/variables/0/name = "mass"
/content/widgetConfig/variables/0/max = 10
```
Patch:
```json
patch_stage({
"target": "/scenes/3",
"intent": "Extend the mass experiment range",
"ops": [
{
"op": "set",
"path": "/content/widgetConfig/variables/0/max",
"value": 20
}
]
})
```
Read back and also check that `default <= max`; the validator does not check it.
## Worked example 3: replace the HTML document
Read source first and confirm this scene uses `html` rather than only `url`.
Patch the exact document string:
```json
patch_stage({
"target": "/scenes/3",
"intent": "Replace the interactive document copy",
"ops": [
{
"op": "set",
"path": "/content/html",
"value": "<!doctype html><html><body><main>New activity</main></body></html>"
}
]
})
```
Read `detail:"source"` to verify exact bytes, then `detail:"text"` to verify
that `New activity` is visible in the projection.
## Hard rules
- Keep the content and widget discriminators consistent.
- Patch leaves; do not replace a config merely to change one label.
- Treat the app TypeScript union as the authoring contract even where the
historical write validator is tolerant.
- Use source search for hidden tests, solutions, ids, and state.
- Read back after every write.
SKILL.md
---
name: stage-dsl
title: "课堂文档结构"
description: The map for reading and editing an OpenMAIC stage document with read_stage, patch_stage, and grep_stage. Load it before patching a structure you have not patched before, when patch_stage rejects an operation, or whenever the path from a stage, outline, scene, content object, or action to the field you need is uncertain. It routes to field-level references for quizzes, interactive widgets, actions, and PBL projects; the installed slide-dsl skill remains the complete slide canvas manual.
---
# The stage document map
This is a map, not the field manual.
Use it to decide which subtree owns a value, which path to read, and which
reference chapter to load. Then read the exact source before writing.
## The document model
The durable structure is:
```text
stage
├── outline
└── scenes[] ordered by scene.order, shown as pages 1..N
├── id stable scene identity
├── order 1-based page position
├── type slide | quiz | interactive | pbl
├── content shape selected by scene.type
│ ├── slide.canvas
│ ├── quiz.questions[]
│ ├── interactive.html / widgetConfig
│ └── pbl.projectV2
└── actions[] ordered playback verbs
```
`stage` is the stage's metadata. `outline` is the generation plan. A persisted page
is a scene. Its `type` and `content.type` must agree.
The three generic tools do not replace page-list operations. Insert, delete,
reorder, and retitle pages with `edit_deck`.
## Tool vocabulary
| Need | Tool | How |
| --- | --- | --- |
| Read a scene | `read_stage` | `path:/scenes/<order|sceneId>` with the required detail |
| Edit scene content or actions | `patch_stage` | `target:/scenes/<order|sceneId>` and scene-root JSON Pointer ops |
| Search visible text or source | `grep_stage` | literal search over the whole stage |
| List stages in folders | `list_folder_stages` | returns the explicit `stageId` required by every stage tool |
| Insert, delete, reorder, or retitle pages | `edit_deck` | page-list operations stay outside the document patcher |
| Plan and build a new stage | conversation + `create_stage` + `generate_scene` | settle the page plan in conversation, then call `generate_scene` once per page with an explicit brief |
| Set the classroom cast | `set_roster` | write the settled roster before page generation |
## Addressing with read_stage
| Path | Resolves to |
| --- | --- |
| `""` or omitted | the whole stage |
| `/outline` | the persisted outline snapshot |
| `/scenes/3` | the scene whose `order` is 3 |
| `/scenes/scene_abc` | the scene with that stable id |
| `/scenes/scene-abc` | the historical hyphenated scene-id form |
| `/scenes/3/actions` | only scene 3's action array |
Orders are 1-based. Array indices inside source JSON are 0-based.
`detail:"tree"` is the compact structural inventory. It reports scene id,
order, type, title, element/question/project counts, and action counts. It is
for finding a target, never for reconstructing a write value.
`detail:"source"` is the exact JSON at the selected path. A scene source is the
persisted scene object, so writable pointers begin `/content/...` or
`/actions/...`. Inline media bytes larger than 2 KiB are replaced in this read
projection by a read-only placeholder. The stored document is unchanged.
`detail:"text"` is the visible-text projection. Use it to find learner-facing
copy or prove that old wording no longer remains. It deliberately omits known
internal PBL prompts and runtime state.
Source and text responses are character-paged after 12,000 characters. Pass
the returned `nextOffset` back as `offset` until it disappears.
## Writing with patch_stage
`target` is one scene path: `/scenes/<order|sceneId>`.
Every call carries a human `intent` and one or more `ops`. The ops are atomic:
the server applies them to a clone, validates the resulting scene, and writes
once. If op 2 fails, op 1 is not persisted.
| Op | Fields | Meaning |
| --- | --- | --- |
| `set` | `path`, `value` | replace an existing leaf or add an optional object key |
| `remove` | `path` | delete an existing object key or splice an array index |
| `str_replace` | `path`, `oldText`, `newText`, optional `replaceAll` | replace one exact occurrence of `oldText` inside the string field at `path`; `replaceAll:true` replaces every occurrence |
| `add_element` | `element`, optional `afterId` or `index` | add one complete id-less slide element |
| `delete_element` | `elementId` | delete one slide element by stable id |
Set/remove/str_replace paths are JSON Pointers rooted at the scene source:
```text
/content/canvas/elements/0/content
/content/questions/1/options/0/label
/content/widgetConfig/description
/content/projectV2/milestones/0/title
/actions/2/text
```
Escape `/` in an object key as `~1` and `~` as `~0`. Array indices are
canonical zero-based integers: `0`, `1`, `2`, never `03`, `-1`, or `+1`.
Every intermediate segment must exist. `set` may create only the final object
key. `remove` requires the final key or array slot to exist.
For a change inside a large HTML document or long text field, prefer
`str_replace` over rewriting the whole field with `set`: transcribing 27 KB of
HTML to change one number is expensive, and any transcription error silently
corrupts the page. Read `detail:"source"`, pick a short unique anchor, replace
it, then read back and `grep_stage` to verify. `oldText` must appear exactly
once in the stored string; on multiple matches extend the anchor or set
`replaceAll:true`. Neither `oldText` nor `newText` may contain a read-side
media omission placeholder; `newText` may be empty to delete the anchor.
Scene metadata is not writable here. Paths must begin `/content/` or
`/actions/`; use `edit_deck` for page metadata and page-list changes.
## Finding with grep_stage
`scope:"text"` searches the visible-text projection. `scope:"source"`
searches serialized scene JSON, including field names and internal data.
Search is literal, case-insensitive, and applies NFKC to both query and source.
Thus half-width `AI` finds full-width `AI`. Result `start` and `end` still slice
the original, unnormalized scene string correctly.
A call returns at most 10 matches per scene and 30 overall, within its time and
character budget. `truncated:true` always includes an opaque `cursor`. Repeat
the same query, scope, and stage with that cursor to continue.
## Read before write
For every edit:
1. Read the target scene with `detail:"source"`.
2. Locate the exact field and array index in that source.
3. Load the matching field-reference chapter below if this structure is new to
you or a previous patch was rejected.
4. Patch the smallest leaf that expresses the intent.
5. Read the same source path again and verify the stored value.
6. Use `detail:"text"` or `grep_stage` when the check is “no old copy remains.”
Never build a patch from `tree`; it intentionally omits neighbouring fields.
Never copy a `<… bytes omitted: …>` media placeholder into a write. Supply a
new real URL/src or leave that field untouched.
## Route to the field manual
| What you need to write | Read this first |
| --- | --- |
| Slide canvas, background, theme, any of the ten slide element types | Read the installed `slide-dsl` skill at the location shown in `<available_skills>`. It is the complete manual and its examples already use scene-root `/content/canvas/...` pointers. |
| Quiz questions, options, answers, grading fields | [`references/quiz.md`](references/quiz.md) |
| Interactive HTML or typed widget configuration | [`references/widget.md`](references/widget.md) |
| Narration, spotlight, whiteboard, video, discussion, or widget playback actions | [`references/actions.md`](references/actions.md) |
| PBL projectV2 roles, milestones, microtasks, packaged design, or runtime-owned fields | [`references/pbl.md`](references/pbl.md) |
## Validation boundary
Slides use the closed slide element schema and reject unknown fields, wrong
types, missing required fields, id changes, and element-type changes.
Quiz writes add a closed question/option check around the current document
validator. Interactive content is closed at its content root, but historical
widgetConfig objects remain intentionally tolerant below that root. PBL is
closed at its content root, while the existing projectV2 validator requires its
core containers and deliberately tolerates historical runtime extension fields.
That difference matters: “accepted” means the current persisted contract
accepted the shape, not that every value is pedagogically sound or every
renderer consumes it. The reference chapters name the hard boundary and the
known semantic boundary separately.
## Hard rules
- Read source before every patch and read it again afterward.
- Patch one leaf when one leaf is enough.
- Use scene-root paths: `/content/...` and `/actions/...`.
- For a change inside a large HTML or long text field, use `str_replace` with a short unique anchor instead of rewriting the whole field.
- Use `add_element` and `delete_element` for slide element identity changes.
- Do not use patch_stage for page insertion, deletion, reordering, or titles.
- Do not write media omission placeholders.
- A rejected atomic batch changed nothing.
- When uncertain, stop guessing and read the matching reference chapter.