diff --git a/COPYRIGHT.md b/COPYRIGHT.md index 7e8e28e..0436478 100644 --- a/COPYRIGHT.md +++ b/COPYRIGHT.md @@ -25,7 +25,7 @@ diff --git a/insider-threat-ncp.json b/insider-threat-ncp.json index c4e7f2a..a6b8d3f 100644 --- a/insider-threat-ncp.json +++ b/insider-threat-ncp.json -@@ -32,6 +32,14 @@ +@@ -32,6 +32,22 @@ "moments": [ + { + "id": "fe8a8863-866d-411d-8d2d-8fb30876abfc", @@ -33,7 +33,21 @@ index c4e7f2a..a6b8d3f 100644 + "synopsis": "Alex meets Miriam, visibly distressed and accusing her staff of betrayal. Alex remains tight-lipped.", + "setting": "Miriam’s private office, late evening.", + "timing": "After hours, amid rising company tensions.", -+ "audience_experiential_pov": "third-person limited", ++ "audience_experiential_pov": "third_person_limited", ++ "storybeats": [ ++ { ++ "sequence": 0, ++ "narrative_id": "narrative_insider_threat", ++ "storybeat_id": "beat_ceo_confrontation" ++ } ++ ], ++ "storypoints": [ ++ { ++ "sequence": 0, ++ "narrative_id": "narrative_insider_threat", ++ "storypoint_id": "storypoint_objective_story_symptom" ++ } ++ ] + } ] ``` diff --git a/HISTORY.md b/HISTORY.md index 3f021c5..d7358aa 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,7 +2,10 @@ ## Recent Schema Updates -- Added optional `story.settings[]` entries for reusable story-level Settings and optional `narratives[].storytelling.moments[].setting_id` references so Moments can point at a shared Setting while preserving free-text `setting`. +- Moved canonical Moments to required `story.moments[]` so storytelling units belong to the story and can reference Storybeats and Storypoints across multiple narratives. +- Added narrative-qualified `story.moments[].storybeats[]` and `story.moments[].storypoints[]` references, requiring `narrative_id` on each referenced structural element. +- Removed `narratives[].storytelling.moments[]` so canonical payloads have one unambiguous Moment home. +- Added optional `story.settings[]` entries for reusable story-level Settings and optional Moment `setting_id` references so Moments can point at a shared Setting while preserving free-text `setting`. - Added a canonical story settings example and refreshed validation fixtures so canonical Storybeats omit legacy `signpost` keys. - Added optional `subtext.storybeats[].appreciation` as a derived interoperability field based on `throughline + scope + sequence`. - Clarified that Storybeat importers should derive the appreciation identity when the field is omitted so lighter-weight payloads remain compatible. diff --git a/README.md b/README.md index 8c35792..51e5835 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ By clearly delineating narrative structure (Subtext) from presentation (Storytel NCP also includes an optional `story.ideation` layer (`character`, `theme`, `plot`, `genre`) so creators can capture early concepts before committing to full storyform structure. +Storytelling Moments live at `story.moments[]` so scenes, chapters, sequences, and levels can reference Storybeats and Storypoints across multiple narratives without duplicating the storytelling unit. + --- ### Authorship, AI, and Creative Intent @@ -67,6 +69,7 @@ Use [/VALIDATION.md](/VALIDATION.md) for validating your own NCP files and CI se ## Templates - [Complete Storyform template](/examples/complete-storyform-template.json): blank-slate NCP fixture with canonical Storypoint Appreciations excluding `Event` and `Progression` labels, plus Signpost-only Storybeats (no Progression/Event Storybeats). `narrative_function` is intentionally omitted so teams can fill in only what they need. +- [Cross-narrative Moments](/examples/cross-narrative-moments.json): story-level Moment fixture showing one scene referencing Storybeats and Storypoints from two different narratives. ## For Adopters (Self-Serve) diff --git a/SPECIFICATION.md b/SPECIFICATION.md index e49915e..d409576 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -18,6 +18,7 @@ This clear distinction encourages narrative depth alongside flexibility, allowin "plot": [], "genre": [] }, + "moments": [], "narratives": [ { "id": "narrative_AbnHJ147", @@ -31,8 +32,7 @@ This clear distinction encourages narrative depth alongside flexibility, allowin "dynamics": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } ] @@ -56,6 +56,7 @@ The highest-level object representing the entire story, containing its metadata "plot": [], "genre": [] }, + "moments": [], "narratives": [], "created_at": "2025-02-05T14:30:00Z" } @@ -71,7 +72,7 @@ The highest-level object representing the entire story, containing its metadata - Each domain is an array of lightweight nodes requiring only `id` and `summary`. - Nodes remain open/extensible so creators and LLM workflows can attach additional metadata without breaking schema compatibility. -This layer informs narratives as projects mature, while keeping strict structural meaning in `narratives[].subtext` and `narratives[].storytelling`. +This layer informs narratives as projects mature, while keeping strict structural meaning in `narratives[].subtext`, `narratives[].storytelling`, and story-level `story.moments[]`. For open-source adopters, this creates a shared on-ramp: communities can exchange early creative concepts in a common format without forcing immediate commitment to full Dramatica Storyform structure, while still preserving interoperability with canonical narrative objects. @@ -127,6 +128,40 @@ This structure provides both depth (meaning) and flexibility (presentation) with "story": { "id": "story_123e4567", "title": "The Journey Within", + "moments": [ + { + "id": "moment_gate_lockdown", + "summary": "The gate locks down.", + "synopsis": "The public evacuation crisis and a private relationship rupture happen in the same audience-facing scene.", + "setting": "The transit gate concourse.", + "timing": "Minutes before the final convoy window closes.", + "imperatives": "Carry both the external evacuation turn and the personal relationship turn.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_AbnHJ147", + "storybeat_id": "beat_public_crisis" + }, + { + "sequence": 1, + "narrative_id": "narrative_MnT90210", + "storybeat_id": "beat_private_rupture" + } + ], + "storypoints": [ + { + "sequence": 0, + "narrative_id": "narrative_AbnHJ147", + "storypoint_id": "storypoint_public_goal" + }, + { + "sequence": 1, + "narrative_id": "narrative_MnT90210", + "storypoint_id": "storypoint_relationship_issue" + } + ] + } + ], "narratives": [ { "id": "narrative_AbnHJ147", @@ -140,8 +175,7 @@ This structure provides both depth (meaning) and flexibility (presentation) with "dynamics": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } ] @@ -194,7 +228,11 @@ Overviews deliver high-level storytelling components, such as Throughline descri **Why?** Overviews help authors clearly communicate their narrative's essential themes and structural direction, ensuring audiences can effortlessly follow and deeply connect with the story. ## Moments -Moments organize storytelling into narrative units like acts, scenes, chapters, or sequences. Each Moment includes a concise synopsis and structured references linking to associated Storybeats, providing clear narrative structure and aiding audience comprehension and engagement. +Moments organize storytelling into narrative units like acts, scenes, chapters, sequences, or levels. Canonical Moments live on `story.moments[]`, not inside a single narrative, because a storytelling unit belongs to the audience-facing story and can carry material from more than one narrative at the same time. A scene can turn the public Objective Story, illustrate a private Relationship Story issue, and echo a Main Character Storypoint without becoming three separate scenes. + +Each story-level Moment includes a concise synopsis and narrative-qualified references to associated Storybeats and Storypoints. The `narrative_id` on each reference preserves which formal narrative owns the structural element while allowing the Moment to gather them into one storytelling unit. + +Canonical payloads should not contain `narratives[].storytelling.moments[]`. Moments have one home: `story.moments[]`. **Why?** Structuring storytelling through Moments ensures narratives are approachable and engaging, helping audiences intuitively grasp story progression and emotional dynamics. @@ -423,6 +461,8 @@ Surface-level narrative elements that quickly orient the audience, such as Logli Organizational narrative units—such as Acts, Scenes, Sequences, Chapters, and Levels—that help structure the narrative temporally. These units can vary in scale and can be flexibly defined to organize narrative flow in any specific context. +Canonical Moments live directly on `story.moments[]`. This makes them story-level storytelling units: they can reference Storybeats and Storypoints from any narrative in the story instead of being trapped inside one narrative's `storytelling` object. + ```json "moments": [ { @@ -438,9 +478,33 @@ Organizational narrative units—such as Acts, Scenes, Sequences, Chapters, and { "type": "space", "limit": 10 } ], "storybeats": [ - { "sequence": 0, "storybeat_id": "beat_123456" }, - { "sequence": 1, "storybeat_id": "beat_789012" }, - { "sequence": 2, "storybeat_id": "beat_345678" } + { + "sequence": 0, + "narrative_id": "narrative_public_crisis", + "storybeat_id": "beat_123456" + }, + { + "sequence": 1, + "narrative_id": "narrative_private_reckoning", + "storybeat_id": "beat_789012" + }, + { + "sequence": 2, + "narrative_id": "narrative_public_crisis", + "storybeat_id": "beat_345678" + } + ], + "storypoints": [ + { + "sequence": 0, + "narrative_id": "narrative_public_crisis", + "storypoint_id": "storypoint_public_goal" + }, + { + "sequence": 1, + "narrative_id": "narrative_private_reckoning", + "storypoint_id": "storypoint_private_issue" + } ] }, { @@ -455,8 +519,23 @@ Organizational narrative units—such as Acts, Scenes, Sequences, Chapters, and { "type": "space", "limit": 20 } ], "storybeats": [ - { "sequence": 0, "storybeat_id": "beat_987654" }, - { "sequence": 1, "storybeat_id": "beat_654321" } + { + "sequence": 0, + "narrative_id": "narrative_public_crisis", + "storybeat_id": "beat_987654" + }, + { + "sequence": 1, + "narrative_id": "narrative_public_crisis", + "storybeat_id": "beat_654321" + } + ], + "storypoints": [ + { + "sequence": 0, + "narrative_id": "narrative_public_crisis", + "storypoint_id": "storypoint_public_revelation" + } ] } ] diff --git a/docs/narrative-context-protocol-schema.md b/docs/narrative-context-protocol-schema.md index 7f05e21..61de11d 100644 --- a/docs/narrative-context-protocol-schema.md +++ b/docs/narrative-context-protocol-schema.md @@ -12,6 +12,7 @@ Use this page when implementing import/export, validation, and cross-tool interc - A shared envelope for transporting narrative context (`schema_version` + `story`). - A consistent separation of `subtext` and `storytelling` per narrative. +- Canonical story-level `moments` that can reference Storybeats and Storypoints across narratives. - Closed canonical narrative shapes, so extra keys are rejected unless a shape explicitly allows extensions. - Canonical enums for `appreciation`, `narrative_function`, `dynamic`, and `vector`. - Optional custom mapping fields that preserve canonical meaning. @@ -47,6 +48,7 @@ Legacy exports are not part of canonical validation. "plot": [], "genre": [] }, + "moments": [], "narratives": [] } } @@ -59,13 +61,65 @@ Required top-level fields: Required `story` fields: -- `id`, `title`, `logline`, `created_at`, `narratives` +- `id`, `title`, `logline`, `created_at`, `moments`, `narratives` Optional `story` fields: - `genre` (concise story label) - `ideation` (pre-narrative beginner/exploratory concept threads) +## Story Moments + +`story.moments[]` is the canonical home for audience-facing storytelling units such as scenes, acts, chapters, sequences, or levels. Moments belong to the story because storytelling units often carry structural material from more than one formal narrative. + +For example, one scene might advance a public evacuation narrative while also turning a mentor-student relationship narrative. Keeping that scene in `story.moments[]` lets the Moment reference both narratives without duplicating the scene or forcing one narrative to own it. + +Required keys per item: + +- `summary`, `synopsis`, `setting`, `timing`, `imperatives`, `storybeats`, `storypoints` + +Optional keys: + +- `id`, `setting_id`, `act`, `order`, `maximum_steps`, `fabric`, `audience_experiential_pov` + +`storybeats` is an ordered list of narrative-qualified Storybeat references: + +```json +"storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_evacuation", + "storybeat_id": "beat_evac_signpost_2" + }, + { + "sequence": 1, + "narrative_id": "narrative_mentor_student", + "storybeat_id": "beat_mentor_signpost_2" + } +] +``` + +`storypoints` is an ordered list of narrative-qualified Storypoint references: + +```json +"storypoints": [ + { + "sequence": 0, + "narrative_id": "narrative_evacuation", + "storypoint_id": "storypoint_evac_goal" + }, + { + "sequence": 1, + "narrative_id": "narrative_mentor_student", + "storypoint_id": "storypoint_mentor_issue" + } +] +``` + +`setting` remains the Moment-specific free-text description. `setting_id` may reference a `story.settings[]` entry when the Moment occurs in a reusable story-level setting. + +Canonical payloads should not contain `narratives[].storytelling.moments[]`. Moments have one home: `story.moments[]`. + ## Ideation Model (Optional Beginner Layer) `story.ideation` is optional. If present, it must contain all four arrays: @@ -277,10 +331,9 @@ When `appreciation` is present on a Storybeat, it should restate the structural ## Storytelling Model -`storytelling` contains two required arrays: +`storytelling` contains one required array: - `overviews` -- `moments` ### Overviews @@ -298,27 +351,6 @@ IDs are opaque strings. Plain UUIDs are fine; type prefixes are optional. Canonical exporters should emit those exact Title Case values. Importers/normalizers may accept legacy inputs such as `logline`, `genre`, `blended_throughlines`, `Premise Overview`, and `Four Throughlines Extraction`, but they should normalize those values before schema validation or export. -### Moments - -Required keys per item: - -- `summary`, `synopsis`, `setting`, `timing`, `imperatives`, `storybeats` - -Optional keys: - -- `id`, `setting_id`, `act`, `order`, `maximum_steps`, `fabric`, `audience_experiential_pov` - -`setting` remains the Moment-specific free-text description. `setting_id` may reference a `story.settings[]` entry when the Moment occurs in a reusable story-level setting. - -`storybeats` inside a moment is an ordered reference list: - -```json -"storybeats": [ - { "sequence": 0, "storybeat_id": "beat_abc123" }, - { "sequence": 1, "storybeat_id": "beat_def456" } -] -``` - ## Canonical Terminology Sources Canonical sets are versioned in two places: diff --git a/examples/complete-space-adventure-storyform.json b/examples/complete-space-adventure-storyform.json index e494624..abf21c5 100644 --- a/examples/complete-space-adventure-storyform.json +++ b/examples/complete-space-adventure-storyform.json @@ -1179,718 +1179,835 @@ "summary": "A frontier science-adventure rescue story with a civic thriller engine.", "storytelling": "The genre promise is ships, stations, impossible routes, and public danger, but the emotional charge comes from whether a pilot can trust a disgraced mentor before the system runs out of ways to survive." } - ], - "moments": [ - { - "id": "moment_anon_0001", - "act": 1, - "order": 1, - "summary": "The False Beacon", - "synopsis": "A distress beacon wakes Harbor Nine and claims the Aster Gate is already tearing open. Nessa Holt is first to launch, but the signal repeats too perfectly, suggesting someone wants the station to panic before the real collapse begins.", - "setting": "Harbor Nine command ring and outer relay lanes.", - "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0006", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0001", - "sequence": 99 - } - ] - }, - { - "id": "moment_anon_0002", - "act": 1, - "order": 2, - "summary": "The Grounding Order", - "synopsis": "Captain Talia Merrow grounds all civilian ships while the council argues over evacuation priority. Nessa sees families locked behind procedure and begins treating the official chain of command as another obstacle to outrun.", - "setting": "Harbor Nine command ring and outer relay lanes.", - "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0006", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0001", - "sequence": 100 - } - ] - }, - { - "id": "moment_anon_0003", - "act": 1, - "order": 3, - "summary": "The Mining Skiff", - "synopsis": "A damaged skiff drifts into the debris lane outside the gate. Nessa breaks formation, rescues the miners, and gives Pike the first evidence he needs to paint her as reckless.", - "setting": "Harbor Nine command ring and outer relay lanes.", - "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0013", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0006", - "sequence": 0 - } - ] - }, - { - "id": "moment_anon_0004", - "act": 1, - "order": 4, - "summary": "Brannic Names the Drift", - "synopsis": "Brannic Orso recognizes the gate distortion from the disaster that ruined him. His warning forces the council to choose between listening to a disgraced navigator or protecting the official story of the old collapse.", - "setting": "Harbor Nine command ring and outer relay lanes.", - "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0014", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0013", - "sequence": 1 - } - ] - }, - { - "id": "moment_anon_0005", - "act": 1, - "order": 5, - "summary": "The Duplicate Key", - "synopsis": "A gate key turns up inside a relief container marked for medical supplies. Nessa and Talia realize someone has been preparing a private escape route while the public evacuation stalls.", - "setting": "Harbor Nine command ring and outer relay lanes.", - "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0016", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0020", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0027", - "sequence": 0 - } - ] - }, - { - "id": "moment_anon_0006", - "act": 1, - "order": 6, - "summary": "The Early Convoy", - "synopsis": "A priority convoy jumps without clearance and tears one of the outer relays loose. The mistake closes several safe routes, proving the evacuation is being destroyed by privilege as much as by physics.", - "setting": "Harbor Nine command ring and outer relay lanes.", - "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0021", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0028", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0014", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0016", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0020", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0027", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0002", - "sequence": 99 - } - ] - }, - { - "id": "moment_anon_0007", - "act": 1, - "order": 7, - "summary": "Pike Points at Nessa", - "synopsis": "Corven Pike uses the station feed to blame Nessa for the stolen key. Nessa nearly answers with another unauthorized flight, but Brannic stops her long enough to ask who benefits from making her look guilty.", - "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", - "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0015", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0007", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0016", - "sequence": 2 - }, - { - "storybeat_id": "beat_anon_0002", - "sequence": 100 - } - ] - }, - { - "id": "moment_anon_0008", - "act": 2, - "order": 8, - "summary": "The Old Route", - "synopsis": "Brannic shows Nessa the route her father tried to fly during the first collapse. Nessa hears, for the first time, that her father may have been following a real pattern rather than fleeing command.", - "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", - "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0023", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0009", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0030", - "sequence": 0 - } - ] - }, - { - "id": "moment_anon_0009", - "act": 2, - "order": 9, - "summary": "Lower Ring Riot", - "synopsis": "Miners discover their families were moved behind corporate passengers. Talia loses control of the boarding lines, and the public conflict becomes a fight over who counts as worth saving.", - "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", - "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0009", - "sequence": 0 - } - ] - }, - { - "id": "moment_anon_0010", - "act": 2, - "order": 10, - "summary": "The Silent Lane Test", - "synopsis": "Nessa and Brannic slip behind the gate to test the unlisted route. Their ship nearly disappears in static, but Nessa glimpses a repeating pulse that matches her father's final transmission.", - "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", - "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0009", - "sequence": 0 - } - ] - }, - { - "id": "moment_anon_0011", - "act": 2, - "order": 11, - "summary": "The Hidden Count", - "synopsis": "Talia admits the council has fewer exits than it claimed. The evacuation can no longer be managed as public reassurance; it has to become a desperate search for any viable lane.", - "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", - "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0009", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0003", - "sequence": 99 - } - ] - }, - { - "id": "moment_anon_0012", - "act": 2, - "order": 12, - "summary": "Medical Ships in the Dark", - "synopsis": "The saboteur redirects medical ships toward a dead corridor. Nessa copies Brannic's manual correction and saves the convoy, proving she can act from trust rather than defiance.", - "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", - "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0018", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0015", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0021", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0023", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0028", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0030", - "sequence": 1 - } - ] - }, - { - "id": "moment_anon_0013", - "act": 2, - "order": 13, - "summary": "Brannic Confesses", - "synopsis": "Brannic admits he sealed the old route before Nessa's father could return. Nessa's anger finally has a target, but the confession also explains why Brannic has spent years preserving forbidden maps.", - "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", - "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0008", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0003", - "sequence": 100 - } - ] - }, - { - "id": "moment_anon_0014", - "act": 2, - "order": 14, - "summary": "Maps Without Forgiveness", - "synopsis": "Nessa refuses to forgive Brannic and still takes his charts. The relationship shifts from emotional judgment to practical dependence, which is the only kind of trust they can manage under pressure.", - "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", - "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0011", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0018", - "sequence": 1 - } - ] - }, - { - "id": "moment_anon_0015", - "act": 2, - "order": 15, - "summary": "Gravity Loss", - "synopsis": "The lower ring loses gravity during family boarding. Talia gives Nessa command of civilian pilots because the station needs someone who will move faster than the council can vote.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0011", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0018", - "sequence": 2 - } - ] - }, - { - "id": "moment_anon_0016", - "act": 2, - "order": 16, - "summary": "The Private Lane Offer", - "synopsis": "Pike offers the council a private lane in exchange for control of the key. His bargain exposes the moral shape of the crisis: survival bought by exclusion will destroy the colony even if ships escape.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0011", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0023", - "sequence": 2 - }, - { - "storybeat_id": "beat_anon_0030", - "sequence": 2 - } - ] - }, - { - "id": "moment_anon_0017", - "act": 2, - "order": 17, - "summary": "Ledger Trace", - "synopsis": "Nessa traces the duplicate key to Pike's convoy ledger. Instead of shouting her innocence, she follows the evidence step by step and lets the record corner him.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0022", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0025", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0011", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0029", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0032", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0004", - "sequence": 99 - } - ] - }, - { - "id": "moment_anon_0018", - "act": 3, - "order": 18, - "summary": "Relay Overload", - "synopsis": "Pike overloads the relay and traps Brannic inside the control spine. The sabotage stops being political and becomes immediate: the only person who understands the drift is now locked inside the failing machine.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0010", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0004", - "sequence": 100 - } - ] - }, - { - "id": "moment_anon_0019", - "act": 3, - "order": 19, - "summary": "Broken Signal Lesson", - "synopsis": "Brannic teaches Nessa through a signal that keeps cutting out. He cannot give her proof, only rhythm, memory, and the courage to recognize a pattern before it resolves.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0017", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0025", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0029", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0032", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0018", - "sequence": 3 - }, - { - "storybeat_id": "beat_anon_0004", - "sequence": 101 - } - ] - }, - { - "id": "moment_anon_0020", - "act": 3, - "order": 20, - "summary": "Nessa Powers Down", - "synopsis": "Nessa turns off the clean instruments and listens for the pulse. Her defining change is quiet rather than spectacular: she stops trying to prove herself and lets the ship drift into the route.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0012", - "sequence": 0 - } - ] - }, - { - "id": "moment_anon_0021", - "act": 3, - "order": 21, - "summary": "First Wave Crossing", - "synopsis": "The first refugee wave follows Nessa through the unstable gate. The colony sees that the forbidden route works, and scattered factions begin acting like one convoy.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0019", - "sequence": 0 - } - ] - }, - { - "id": "moment_anon_0022", - "act": 3, - "order": 22, - "summary": "Shock Front", - "synopsis": "A shock front knocks Nessa away from the lane. Her old instinct is to fight the controls, but the new route demands restraint while everyone watches.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0017", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0019", - "sequence": 1 - } - ] - }, - { - "id": "moment_anon_0023", - "act": 3, - "order": 23, - "summary": "Hold Formation", - "synopsis": "Talia orders the convoy to hold instead of scatter. The public story turns when the fleet chooses trust over individual escape, buying Nessa the seconds she needs.", - "setting": "Lower ring, convoy lanes, and the failing control spine.", - "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0012", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0019", - "sequence": 2 - } - ] - }, - { - "id": "moment_anon_0024", - "act": 3, - "order": 24, - "summary": "Manual Lock", - "synopsis": "Brannic releases the lock from inside the control spine. He accepts the risk he once avoided, giving Nessa the opening her father never received.", - "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", - "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0012", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0019", - "sequence": 3 - } - ] - }, - { - "id": "moment_anon_0025", - "act": 3, - "order": 25, - "summary": "Harbor Nine Breaks", - "synopsis": "The last convoy clears as Harbor Nine tears free of its moorings. The old home is lost, but the people escape with a shared route and a shared account of what happened.", - "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", - "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0024", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0026", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0031", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0033", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0022", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0025", - "sequence": 2 - }, - { - "storybeat_id": "beat_anon_0032", - "sequence": 2 - }, - { - "storybeat_id": "beat_anon_0005", - "sequence": 99 - } - ] - }, - { - "id": "moment_anon_0026", - "act": 3, - "order": 26, - "summary": "Return Run", - "synopsis": "Nessa turns back for Brannic instead of taking the broadcast victory. She chooses relationship over reputation, proving her peace no longer depends on being publicly vindicated.", - "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", - "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0024", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0026", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0033", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0005", - "sequence": 100 - } - ] - }, - { - "id": "moment_anon_0027", - "act": 3, - "order": 27, - "summary": "Pike's Empty Lane", - "synopsis": "Pike reaches his private lane and finds no one willing to follow. His failure is social before it is tactical: a route built for exclusion has no future colony inside it.", - "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", - "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0012", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0005", - "sequence": 101 - } - ] - }, - { - "id": "moment_anon_0028", - "act": 3, - "order": 28, - "summary": "Shared Coordinates", - "synopsis": "Nessa and Brannic combine their data into a stable route map. The rescue becomes more than survival when their shared coordinates can help other frontier stations before they face the same collapse.", - "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", - "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0012", - "sequence": 0 - }, - { - "storybeat_id": "beat_anon_0026", - "sequence": 2 - }, - { - "storybeat_id": "beat_anon_0033", - "sequence": 2 - }, - { - "storybeat_id": "beat_anon_0032", - "sequence": 3 - }, - { - "storybeat_id": "beat_anon_0005", - "sequence": 102 - } - ] - }, - { - "id": "moment_anon_0029", - "act": 4, - "order": 29, - "summary": "The Real Name", - "synopsis": "Nessa files her father's route under its original call sign. The record changes from accusation to inheritance, allowing her to carry the past without performing for it.", - "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", - "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [] - }, - { - "id": "moment_anon_0030", - "act": 4, - "order": 30, - "summary": "New Pilots", - "synopsis": "Nessa begins training pilots on the route no one trusted. The story ends with a new discipline: fast action grounded in trust, memory, and responsibility to the people still waiting at the next gate.", - "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", - "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", - "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", - "storybeats": [ - { - "storybeat_id": "beat_anon_0031", - "sequence": 1 - }, - { - "storybeat_id": "beat_anon_0033", - "sequence": 3 - } - ] - } ] } } + ], + "moments": [ + { + "id": "moment_anon_0001", + "act": 1, + "order": 1, + "summary": "The False Beacon", + "synopsis": "A distress beacon wakes Harbor Nine and claims the Aster Gate is already tearing open. Nessa Holt is first to launch, but the signal repeats too perfectly, suggesting someone wants the station to panic before the real collapse begins.", + "setting": "Harbor Nine command ring and outer relay lanes.", + "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0006" + }, + { + "sequence": 99, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0001" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0002", + "act": 1, + "order": 2, + "summary": "The Grounding Order", + "synopsis": "Captain Talia Merrow grounds all civilian ships while the council argues over evacuation priority. Nessa sees families locked behind procedure and begins treating the official chain of command as another obstacle to outrun.", + "setting": "Harbor Nine command ring and outer relay lanes.", + "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0006" + }, + { + "sequence": 100, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0001" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0003", + "act": 1, + "order": 3, + "summary": "The Mining Skiff", + "synopsis": "A damaged skiff drifts into the debris lane outside the gate. Nessa breaks formation, rescues the miners, and gives Pike the first evidence he needs to paint her as reckless.", + "setting": "Harbor Nine command ring and outer relay lanes.", + "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0013" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0006" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0004", + "act": 1, + "order": 4, + "summary": "Brannic Names the Drift", + "synopsis": "Brannic Orso recognizes the gate distortion from the disaster that ruined him. His warning forces the council to choose between listening to a disgraced navigator or protecting the official story of the old collapse.", + "setting": "Harbor Nine command ring and outer relay lanes.", + "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0014" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0013" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0005", + "act": 1, + "order": 5, + "summary": "The Duplicate Key", + "synopsis": "A gate key turns up inside a relief container marked for medical supplies. Nessa and Talia realize someone has been preparing a private escape route while the public evacuation stalls.", + "setting": "Harbor Nine command ring and outer relay lanes.", + "timing": "During evacuation hour 1 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0016" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0020" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0027" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0006", + "act": 1, + "order": 6, + "summary": "The Early Convoy", + "synopsis": "A priority convoy jumps without clearance and tears one of the outer relays loose. The mistake closes several safe routes, proving the evacuation is being destroyed by privilege as much as by physics.", + "setting": "Harbor Nine command ring and outer relay lanes.", + "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0021" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0028" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0014" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0016" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0020" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0027" + }, + { + "sequence": 99, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0002" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0007", + "act": 1, + "order": 7, + "summary": "Pike Points at Nessa", + "synopsis": "Corven Pike uses the station feed to blame Nessa for the stolen key. Nessa nearly answers with another unauthorized flight, but Brannic stops her long enough to ask who benefits from making her look guilty.", + "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", + "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0015" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0007" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0016" + }, + { + "sequence": 100, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0002" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0008", + "act": 2, + "order": 8, + "summary": "The Old Route", + "synopsis": "Brannic shows Nessa the route her father tried to fly during the first collapse. Nessa hears, for the first time, that her father may have been following a real pattern rather than fleeing command.", + "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", + "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0023" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0009" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0030" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0009", + "act": 2, + "order": 9, + "summary": "Lower Ring Riot", + "synopsis": "Miners discover their families were moved behind corporate passengers. Talia loses control of the boarding lines, and the public conflict becomes a fight over who counts as worth saving.", + "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", + "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0009" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0010", + "act": 2, + "order": 10, + "summary": "The Silent Lane Test", + "synopsis": "Nessa and Brannic slip behind the gate to test the unlisted route. Their ship nearly disappears in static, but Nessa glimpses a repeating pulse that matches her father's final transmission.", + "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", + "timing": "During evacuation hour 2 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0009" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0011", + "act": 2, + "order": 11, + "summary": "The Hidden Count", + "synopsis": "Talia admits the council has fewer exits than it claimed. The evacuation can no longer be managed as public reassurance; it has to become a desperate search for any viable lane.", + "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", + "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0009" + }, + { + "sequence": 99, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0003" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0012", + "act": 2, + "order": 12, + "summary": "Medical Ships in the Dark", + "synopsis": "The saboteur redirects medical ships toward a dead corridor. Nessa copies Brannic's manual correction and saves the convoy, proving she can act from trust rather than defiance.", + "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", + "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0018" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0015" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0021" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0023" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0028" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0030" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0013", + "act": 2, + "order": 13, + "summary": "Brannic Confesses", + "synopsis": "Brannic admits he sealed the old route before Nessa's father could return. Nessa's anger finally has a target, but the confession also explains why Brannic has spent years preserving forbidden maps.", + "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", + "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0008" + }, + { + "sequence": 100, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0003" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0014", + "act": 2, + "order": 14, + "summary": "Maps Without Forgiveness", + "synopsis": "Nessa refuses to forgive Brannic and still takes his charts. The relationship shifts from emotional judgment to practical dependence, which is the only kind of trust they can manage under pressure.", + "setting": "Evacuation docks, council chamber, and the hidden route behind the Aster Gate.", + "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0011" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0018" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0015", + "act": 2, + "order": 15, + "summary": "Gravity Loss", + "synopsis": "The lower ring loses gravity during family boarding. Talia gives Nessa command of civilian pilots because the station needs someone who will move faster than the council can vote.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 3 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0011" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0018" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0016", + "act": 2, + "order": 16, + "summary": "The Private Lane Offer", + "synopsis": "Pike offers the council a private lane in exchange for control of the key. His bargain exposes the moral shape of the crisis: survival bought by exclusion will destroy the colony even if ships escape.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0011" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0023" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0030" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0017", + "act": 2, + "order": 17, + "summary": "Ledger Trace", + "synopsis": "Nessa traces the duplicate key to Pike's convoy ledger. Instead of shouting her innocence, she follows the evidence step by step and lets the record corner him.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0022" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0025" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0011" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0029" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0032" + }, + { + "sequence": 99, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0004" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0018", + "act": 3, + "order": 18, + "summary": "Relay Overload", + "synopsis": "Pike overloads the relay and traps Brannic inside the control spine. The sabotage stops being political and becomes immediate: the only person who understands the drift is now locked inside the failing machine.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0010" + }, + { + "sequence": 100, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0004" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0019", + "act": 3, + "order": 19, + "summary": "Broken Signal Lesson", + "synopsis": "Brannic teaches Nessa through a signal that keeps cutting out. He cannot give her proof, only rhythm, memory, and the courage to recognize a pattern before it resolves.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0017" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0025" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0029" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0032" + }, + { + "sequence": 3, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0018" + }, + { + "sequence": 101, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0004" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0020", + "act": 3, + "order": 20, + "summary": "Nessa Powers Down", + "synopsis": "Nessa turns off the clean instruments and listens for the pulse. Her defining change is quiet rather than spectacular: she stops trying to prove herself and lets the ship drift into the route.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 4 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0012" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0021", + "act": 3, + "order": 21, + "summary": "First Wave Crossing", + "synopsis": "The first refugee wave follows Nessa through the unstable gate. The colony sees that the forbidden route works, and scattered factions begin acting like one convoy.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0019" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0022", + "act": 3, + "order": 22, + "summary": "Shock Front", + "synopsis": "A shock front knocks Nessa away from the lane. Her old instinct is to fight the controls, but the new route demands restraint while everyone watches.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0017" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0019" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0023", + "act": 3, + "order": 23, + "summary": "Hold Formation", + "synopsis": "Talia orders the convoy to hold instead of scatter. The public story turns when the fleet chooses trust over individual escape, buying Nessa the seconds she needs.", + "setting": "Lower ring, convoy lanes, and the failing control spine.", + "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0012" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0019" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0024", + "act": 3, + "order": 24, + "summary": "Manual Lock", + "synopsis": "Brannic releases the lock from inside the control spine. He accepts the risk he once avoided, giving Nessa the opening her father never received.", + "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", + "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0012" + }, + { + "sequence": 3, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0019" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0025", + "act": 3, + "order": 25, + "summary": "Harbor Nine Breaks", + "synopsis": "The last convoy clears as Harbor Nine tears free of its moorings. The old home is lost, but the people escape with a shared route and a shared account of what happened.", + "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", + "timing": "During evacuation hour 5 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0024" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0026" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0031" + }, + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0033" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0022" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0025" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0032" + }, + { + "sequence": 99, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0005" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0026", + "act": 3, + "order": 26, + "summary": "Return Run", + "synopsis": "Nessa turns back for Brannic instead of taking the broadcast victory. She chooses relationship over reputation, proving her peace no longer depends on being publicly vindicated.", + "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", + "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0024" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0026" + }, + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0033" + }, + { + "sequence": 100, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0005" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0027", + "act": 3, + "order": 27, + "summary": "Pike's Empty Lane", + "synopsis": "Pike reaches his private lane and finds no one willing to follow. His failure is social before it is tactical: a route built for exclusion has no future colony inside it.", + "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", + "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0012" + }, + { + "sequence": 101, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0005" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0028", + "act": 3, + "order": 28, + "summary": "Shared Coordinates", + "synopsis": "Nessa and Brannic combine their data into a stable route map. The rescue becomes more than survival when their shared coordinates can help other frontier stations before they face the same collapse.", + "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", + "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0012" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0026" + }, + { + "sequence": 2, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0033" + }, + { + "sequence": 3, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0032" + }, + { + "sequence": 102, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0005" + } + ], + "storypoints": [] + }, + { + "id": "moment_anon_0029", + "act": 4, + "order": 29, + "summary": "The Real Name", + "synopsis": "Nessa files her father's route under its original call sign. The record changes from accusation to inheritance, allowing her to carry the past without performing for it.", + "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", + "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [], + "storypoints": [] + }, + { + "id": "moment_anon_0030", + "act": 4, + "order": 30, + "summary": "New Pilots", + "synopsis": "Nessa begins training pilots on the route no one trusted. The story ends with a new discipline: fast action grounded in trust, memory, and responsibility to the people still waiting at the next gate.", + "setting": "Aster Gate crossing, refugee convoy, and the new frontier route.", + "timing": "During evacuation hour 6 of the final Harbor Nine crisis.", + "imperatives": "Move the convoy, protect the vulnerable, expose the sabotage, and decide whether trust can arrive before certainty.", + "storybeats": [ + { + "sequence": 1, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0031" + }, + { + "sequence": 3, + "narrative_id": "narrative_anon_0001", + "storybeat_id": "beat_anon_0033" + } + ], + "storypoints": [] + } ] } } diff --git a/examples/complete-storyform-template.json b/examples/complete-storyform-template.json index f3aa5fe..1050e8a 100644 --- a/examples/complete-storyform-template.json +++ b/examples/complete-storyform-template.json @@ -1398,10 +1398,10 @@ ] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/cross-narrative-moments.json b/examples/cross-narrative-moments.json new file mode 100644 index 0000000..0112474 --- /dev/null +++ b/examples/cross-narrative-moments.json @@ -0,0 +1,113 @@ +{ + "schema_version": "1.3.0", + "story": { + "id": "story_cross_narrative_demo", + "title": "Cross-Narrative Moment Demo", + "logline": "A public evacuation crisis and a mentor-student rupture collide in the same scene.", + "created_at": "2026-06-03T00:00:00Z", + "narratives": [ + { + "id": "narrative_evacuation", + "title": "Public Evacuation", + "subtext": { + "perspectives": [], + "players": [], + "dynamics": [], + "storypoints": [ + { + "id": "storypoint_evac_goal", + "appreciation": "Story Goal", + "illustration": "getting the last convoy through the gate", + "summary": "The public crisis depends on completing the evacuation.", + "storytelling": "Crowds push toward the only working gate as the route destabilizes.", + "perspectives": [] + } + ], + "storybeats": [ + { + "id": "beat_evac_signpost_2", + "scope": "signpost", + "sequence": 2, + "throughline": "Objective Story", + "summary": "The evacuation shifts from orderly processing to a contested emergency.", + "storytelling": "The concourse locks down just as the convoy window begins to close.", + "perspectives": [] + } + ] + }, + "storytelling": { + "overviews": [] + } + }, + { + "id": "narrative_mentor_student", + "title": "Mentor and Student", + "subtext": { + "perspectives": [], + "players": [], + "dynamics": [], + "storypoints": [ + { + "id": "storypoint_mentor_issue", + "appreciation": "Relationship Story Issue", + "illustration": "trusting an old lesson after it has failed once", + "summary": "The relationship turns on whether past instruction still deserves trust.", + "storytelling": "The student repeats the mentor's words back as accusation and plea.", + "perspectives": [] + } + ], + "storybeats": [ + { + "id": "beat_mentor_signpost_2", + "scope": "signpost", + "sequence": 2, + "throughline": "Relationship Story", + "summary": "The mentor-student bond fractures under shared pressure.", + "storytelling": "The student refuses the mentor's order at the exact moment the public crisis peaks.", + "perspectives": [] + } + ] + }, + "storytelling": { + "overviews": [] + } + } + ], + "moments": [ + { + "id": "moment_gate_lockdown", + "act": 2, + "order": 7, + "summary": "The gate locks down.", + "synopsis": "The public evacuation crisis and the private mentor-student rupture happen in the same audience-facing scene.", + "setting": "The transit gate concourse as alarms cut through the crowd noise.", + "timing": "Minutes before the final convoy window closes.", + "imperatives": "Make the scene carry both the external evacuation turn and the personal relationship turn.", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_evacuation", + "storybeat_id": "beat_evac_signpost_2" + }, + { + "sequence": 1, + "narrative_id": "narrative_mentor_student", + "storybeat_id": "beat_mentor_signpost_2" + } + ], + "storypoints": [ + { + "sequence": 0, + "narrative_id": "narrative_evacuation", + "storypoint_id": "storypoint_evac_goal" + }, + { + "sequence": 1, + "narrative_id": "narrative_mentor_student", + "storypoint_id": "storypoint_mentor_issue" + } + ] + } + ] + } +} diff --git a/examples/example-story.json b/examples/example-story.json index 5444667..7d3f280 100644 --- a/examples/example-story.json +++ b/examples/example-story.json @@ -145,29 +145,31 @@ "summary": "Mystery pacing with thriller turns built on memory and regret.", "storytelling": "Each reveal tightens the emotional stakes as the genre shifts between introspection and pursuit." } - ], - "moments": [ - { - "id": "moment_001", - "act": 1, - "order": 1, - "maximum_steps": 3, - "summary": "The inciting discovery", - "synopsis": "John uncovers an old case file that hints at deeper connections to his own past.", - "setting": "A dimly lit precinct archive room.", - "setting_id": "setting_precinct_archive", - "timing": "Late night, after hours.", - "imperatives": "Investigate the link before the evidence disappears.", - "storybeats": [ - { - "sequence": 1, - "storybeat_id": "beat_001" - } - ] - } ] } } + ], + "moments": [ + { + "id": "moment_001", + "act": 1, + "order": 1, + "maximum_steps": 3, + "summary": "The inciting discovery", + "synopsis": "John uncovers an old case file that hints at deeper connections to his own past.", + "setting": "A dimly lit precinct archive room.", + "setting_id": "setting_precinct_archive", + "timing": "Late night, after hours.", + "imperatives": "Investigate the link before the evidence disappears.", + "storybeats": [ + { + "sequence": 1, + "narrative_id": "narrative_001", + "storybeat_id": "beat_001" + } + ], + "storypoints": [] + } ] } } diff --git a/examples/ideation-beginner.json b/examples/ideation-beginner.json index 3586be1..c46d312 100644 --- a/examples/ideation-beginner.json +++ b/examples/ideation-beginner.json @@ -61,10 +61,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/invalid/ideation-missing-domain.json b/examples/invalid/ideation-missing-domain.json index 82606a4..bbd6291 100644 --- a/examples/invalid/ideation-missing-domain.json +++ b/examples/invalid/ideation-missing-domain.json @@ -39,10 +39,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/invalid/ideation-node-missing-summary.json b/examples/invalid/ideation-node-missing-summary.json index 0225538..81f2e0d 100644 --- a/examples/invalid/ideation-node-missing-summary.json +++ b/examples/invalid/ideation-node-missing-summary.json @@ -44,10 +44,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/invalid/moment-storybeat-missing-narrative.json b/examples/invalid/moment-storybeat-missing-narrative.json new file mode 100644 index 0000000..dae636f --- /dev/null +++ b/examples/invalid/moment-storybeat-missing-narrative.json @@ -0,0 +1,42 @@ +{ + "schema_version": "1.3.0", + "story": { + "id": "story_invalid_moment_ref", + "title": "Invalid Moment Reference", + "logline": "A story-level moment must qualify storybeat references with narrative_id.", + "created_at": "2026-06-03T00:00:00Z", + "narratives": [ + { + "id": "narrative_001", + "title": "Central Narrative", + "subtext": { + "perspectives": [], + "players": [], + "dynamics": [], + "storypoints": [], + "storybeats": [] + }, + "storytelling": { + "overviews": [] + } + } + ], + "moments": [ + { + "id": "moment_invalid_ref", + "summary": "A malformed story-level moment.", + "synopsis": "The storybeat reference omits narrative_id.", + "setting": "A validation fixture.", + "timing": "During schema checks.", + "imperatives": "Fail validation.", + "storybeats": [ + { + "sequence": 0, + "storybeat_id": "beat_001" + } + ], + "storypoints": [] + } + ] + } +} diff --git a/examples/invalid/narrative-local-moments.json b/examples/invalid/narrative-local-moments.json new file mode 100644 index 0000000..b2b3311 --- /dev/null +++ b/examples/invalid/narrative-local-moments.json @@ -0,0 +1,27 @@ +{ + "schema_version": "1.3.0", + "story": { + "id": "story_invalid_narrative_moments", + "title": "Invalid Narrative-Local Moments", + "logline": "Moments must live on the story node, not inside a narrative.", + "created_at": "2026-06-03T00:00:00Z", + "narratives": [ + { + "id": "narrative_001", + "title": "Central Narrative", + "subtext": { + "perspectives": [], + "players": [], + "dynamics": [], + "storypoints": [], + "storybeats": [] + }, + "storytelling": { + "overviews": [], + "moments": [] + } + } + ], + "moments": [] + } +} diff --git a/examples/invalid/narrative-status-invalid.json b/examples/invalid/narrative-status-invalid.json index 9dfa8cc..bcec2c9 100644 --- a/examples/invalid/narrative-status-invalid.json +++ b/examples/invalid/narrative-status-invalid.json @@ -45,10 +45,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/invalid/overview-label-legacy.json b/examples/invalid/overview-label-legacy.json index 950d3b1..13f460f 100644 --- a/examples/invalid/overview-label-legacy.json +++ b/examples/invalid/overview-label-legacy.json @@ -25,10 +25,10 @@ "summary": "A test overview.", "storytelling": "Legacy overview labels should be normalized before validation." } - ], - "moments": [] + ] } } - ] + ], + "moments": [] } } diff --git a/examples/invalid/overview-label-snake-case-legacy.json b/examples/invalid/overview-label-snake-case-legacy.json index 0a52884..bc72a2f 100644 --- a/examples/invalid/overview-label-snake-case-legacy.json +++ b/examples/invalid/overview-label-snake-case-legacy.json @@ -25,10 +25,10 @@ "summary": "A test overview.", "storytelling": "Legacy snake_case labels should be normalized before validation." } - ], - "moments": [] + ] } } - ] + ], + "moments": [] } } diff --git a/examples/invalid/perspective-extra-key.json b/examples/invalid/perspective-extra-key.json index 133a1f7..e9b5609 100644 --- a/examples/invalid/perspective-extra-key.json +++ b/examples/invalid/perspective-extra-key.json @@ -26,10 +26,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/invalid/signpost-sequence-out-of-range.json b/examples/invalid/signpost-sequence-out-of-range.json index 1deb195..0a4bd94 100644 --- a/examples/invalid/signpost-sequence-out-of-range.json +++ b/examples/invalid/signpost-sequence-out-of-range.json @@ -83,24 +83,26 @@ "summary": "", "storytelling": "" } - ], - "moments": [ - { - "summary": "", - "synopsis": "", - "setting": "", - "timing": "", - "imperatives": "", - "storybeats": [ - { - "sequence": 0, - "storybeat_id": "beat_invalid_001" - } - ] - } ] } } + ], + "moments": [ + { + "summary": "", + "synopsis": "", + "setting": "", + "timing": "", + "imperatives": "", + "storybeats": [ + { + "sequence": 0, + "narrative_id": "narrative_invalid_001", + "storybeat_id": "beat_invalid_001" + } + ], + "storypoints": [] + } ] } } diff --git a/examples/invalid/storypoint-extra-key.json b/examples/invalid/storypoint-extra-key.json index 7e7b937..98c04c7 100644 --- a/examples/invalid/storypoint-extra-key.json +++ b/examples/invalid/storypoint-extra-key.json @@ -28,10 +28,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/invalid/storypoint-throughline-shorthand.json b/examples/invalid/storypoint-throughline-shorthand.json index 2611bea..4f50e5c 100644 --- a/examples/invalid/storypoint-throughline-shorthand.json +++ b/examples/invalid/storypoint-throughline-shorthand.json @@ -28,10 +28,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/story-settings.json b/examples/story-settings.json index eb1748c..856ba73 100644 --- a/examples/story-settings.json +++ b/examples/story-settings.json @@ -24,21 +24,22 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [ - { - "id": "moment_archive_return", - "summary": "The detective returns to the archive.", - "synopsis": "A second visit to the archive reframes the old evidence as a personal warning.", - "setting": "The same archive room, now colder and less familiar.", - "setting_id": "setting_precinct_archive", - "timing": "After midnight.", - "imperatives": "Show that the shared place has taken on new meaning.", - "storybeats": [] - } - ] + "overviews": [] } } + ], + "moments": [ + { + "id": "moment_archive_return", + "summary": "The detective returns to the archive.", + "synopsis": "A second visit to the archive reframes the old evidence as a personal warning.", + "setting": "The same archive room, now colder and less familiar.", + "setting_id": "setting_precinct_archive", + "timing": "After midnight.", + "imperatives": "Show that the shared place has taken on new meaning.", + "storybeats": [], + "storypoints": [] + } ] } } diff --git a/examples/storypoint-throughline-both-refs.json b/examples/storypoint-throughline-both-refs.json index 7baa420..4d3c26f 100644 --- a/examples/storypoint-throughline-both-refs.json +++ b/examples/storypoint-throughline-both-refs.json @@ -40,10 +40,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/examples/storypoint-throughline-empty-perspectives.json b/examples/storypoint-throughline-empty-perspectives.json index f7a2177..b220b04 100644 --- a/examples/storypoint-throughline-empty-perspectives.json +++ b/examples/storypoint-throughline-empty-perspectives.json @@ -29,10 +29,10 @@ "storybeats": [] }, "storytelling": { - "overviews": [], - "moments": [] + "overviews": [] } } - ] + ], + "moments": [] } } diff --git a/schema/ncp-schema.json b/schema/ncp-schema.json index 54c59ae..35c4f63 100644 --- a/schema/ncp-schema.json +++ b/schema/ncp-schema.json @@ -522,115 +522,10 @@ ], "additionalProperties": false } - }, - "moments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "synopsis": { - "type": "string" - }, - "setting": { - "type": "string" - }, - "setting_id": { - "$ref": "#/$defs/setting_id", - "description": "Optional reference to a story.settings[] entry for this Moment. Use with setting to avoid repeating shared place details while preserving the Moment-specific setting prose." - }, - "timing": { - "type": "string" - }, - "imperatives": { - "type": "string" - }, - "act": { - "type": "integer", - "minimum": 1 - }, - "order": { - "type": "integer", - "minimum": 0 - }, - "maximum_steps": { - "type": "integer", - "minimum": 1 - }, - "fabric": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "space", - "time" - ] - }, - "limit": { - "type": "integer" - } - }, - "required": [ - "type", - "limit" - ], - "additionalProperties": false - } - }, - "audience_experiential_pov": { - "type": "string", - "enum": [ - "first_person_central", - "first_person_peripheral", - "second_person", - "third_person_limited", - "third_person_objective", - "third_person_omniscient" - ] - }, - "storybeats": { - "type": "array", - "items": { - "type": "object", - "properties": { - "sequence": { - "type": "integer" - }, - "storybeat_id": { - "$ref": "#/$defs/stable_id" - } - }, - "required": [ - "sequence", - "storybeat_id" - ], - "additionalProperties": false - } - } - }, - "required": [ - "summary", - "synopsis", - "setting", - "timing", - "imperatives", - "storybeats" - ], - "additionalProperties": false - } } }, "required": [ - "overviews", - "moments" + "overviews" ], "additionalProperties": false } @@ -643,6 +538,13 @@ ], "additionalProperties": false } + }, + "moments": { + "type": "array", + "description": "Canonical story-level storytelling units. Moments should live on story.moments[] so they can relate Storybeats and Storypoints across one or more narratives.", + "items": { + "$ref": "#/$defs/story_moment" + } } }, "required": [ @@ -650,6 +552,7 @@ "title", "logline", "created_at", + "moments", "narratives" ], "additionalProperties": false @@ -1359,6 +1262,140 @@ "Worry", "Worth" ] + }, + "story_moment": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "synopsis": { + "type": "string" + }, + "setting": { + "type": "string" + }, + "setting_id": { + "$ref": "#/$defs/setting_id", + "description": "Optional reference to a story.settings[] entry for this Moment. Use with setting to avoid repeating shared place details while preserving the Moment-specific setting prose." + }, + "timing": { + "type": "string" + }, + "imperatives": { + "type": "string" + }, + "act": { + "type": "integer", + "minimum": 1 + }, + "order": { + "type": "integer", + "minimum": 0 + }, + "maximum_steps": { + "type": "integer", + "minimum": 1 + }, + "fabric": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "space", + "time" + ] + }, + "limit": { + "type": "integer" + } + }, + "required": [ + "type", + "limit" + ], + "additionalProperties": false + } + }, + "audience_experiential_pov": { + "type": "string", + "enum": [ + "first_person_central", + "first_person_peripheral", + "second_person", + "third_person_limited", + "third_person_objective", + "third_person_omniscient" + ] + }, + "storybeats": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sequence": { + "type": "integer" + }, + "storybeat_id": { + "$ref": "#/$defs/stable_id" + }, + "narrative_id": { + "type": "string", + "description": "The id of the narrative that owns the referenced Storybeat." + } + }, + "required": [ + "sequence", + "narrative_id", + "storybeat_id" + ], + "additionalProperties": false + }, + "description": "Ordered references to narrative Storybeats touched by this Moment. Each reference is qualified with narrative_id because story-level Moments can span multiple narratives." + }, + "storypoints": { + "type": "array", + "description": "Ordered references to narrative Storypoints illustrated, invoked, or turned by this Moment. Each reference is qualified with narrative_id because story-level Moments can span multiple narratives.", + "items": { + "type": "object", + "properties": { + "sequence": { + "type": "integer" + }, + "narrative_id": { + "type": "string", + "description": "The id of the narrative that owns the referenced Storypoint." + }, + "storypoint_id": { + "type": "string" + } + }, + "required": [ + "sequence", + "narrative_id", + "storypoint_id" + ], + "additionalProperties": false + } + } + }, + "required": [ + "summary", + "synopsis", + "setting", + "timing", + "imperatives", + "storybeats", + "storypoints" + ], + "additionalProperties": false, + "description": "A story-level storytelling unit such as an act, scene, chapter, sequence, level, or comparable audience-facing unit. Story-level Moments can reference Storybeats and Storypoints from one or more narratives." } } } diff --git a/schema/ncp-schema.yaml b/schema/ncp-schema.yaml index 6ebc2a5..b1bedd4 100644 --- a/schema/ncp-schema.yaml +++ b/schema/ncp-schema.yaml @@ -1,6 +1,7 @@ "$schema": http://json-schema.org/draft-07/schema# title: Narrative Context Protocol Schema -description: A standardized protocol for structuring narrative elements in a complete story for use in multi-agentic systems. +description: A standardized protocol for structuring narrative elements in a complete + story for use in multi-agentic systems. type: object properties: schema_version: @@ -24,7 +25,9 @@ properties: pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" settings: type: array - description: Optional story-level setting glossary. Each entry is a stable, referenceable place or environment that Moments may identify with setting_id while retaining free-text setting prose. + description: Optional story-level setting glossary. Each entry is + a stable, referenceable place or environment that Moments may + identify with setting_id while retaining free-text setting prose. items: type: object properties: @@ -42,7 +45,8 @@ properties: additionalProperties: false ideation: type: object - description: Optional pre-narrative ideation threads for exploratory and beginner workflows. + description: Optional pre-narrative ideation threads for exploratory + and beginner workflows. properties: character: type: array @@ -70,7 +74,9 @@ properties: type: array items: type: object - description: 'A narrative is a Dramatica storyform: a single, complete argument structure within the story, expressed through subtext and storytelling layers.' + description: 'A narrative is a Dramatica storyform: a single, + complete argument structure within the story, expressed through + subtext and storytelling layers.' properties: id: type: string @@ -78,7 +84,8 @@ properties: type: string status: type: string - description: Optional narrative lifecycle state. When omitted, consumers may treat as complete. + description: Optional narrative lifecycle state. When + omitted, consumers may treat as complete. enum: - candidate - draft @@ -247,7 +254,10 @@ properties: "$ref": "#/$defs/perspective_link" throughline: type: string - description: Optional throughline label for grouping and round-trip stability when perspective refs are absent. + description: Optional throughline + label for grouping and round-trip + stability when perspective refs + are absent. enum: - Objective Story - Main Character @@ -282,7 +292,10 @@ properties: "$ref": "#/$defs/stable_id" appreciation: type: string - description: Optional derived structural label, typically throughline + scope + sequence (for example, Objective Story Signpost 1). + description: Optional derived structural + label, typically throughline + + scope + sequence (for example, + Objective Story Signpost 1). scope: type: string enum: @@ -294,7 +307,8 @@ properties: minimum: 1 throughline: type: string - description: Optional throughline label for grouping. + description: Optional throughline + label for grouping. narrative_function: "$ref": "#/$defs/canonical_narrative_function" summary: @@ -373,84 +387,8 @@ properties: - summary - storytelling additionalProperties: false - moments: - type: array - items: - type: object - properties: - id: - type: string - summary: - type: string - synopsis: - type: string - setting: - type: string - setting_id: - "$ref": "#/$defs/setting_id" - description: Optional reference to a story.settings[] entry for this Moment. Use with setting to avoid repeating shared place details while preserving the Moment-specific setting prose. - timing: - type: string - imperatives: - type: string - act: - type: integer - minimum: 1 - order: - type: integer - minimum: 0 - maximum_steps: - type: integer - minimum: 1 - fabric: - type: array - items: - type: object - properties: - type: - type: string - enum: - - space - - time - limit: - type: integer - required: - - type - - limit - additionalProperties: false - audience_experiential_pov: - type: string - enum: - - first_person_central - - first_person_peripheral - - second_person - - third_person_limited - - third_person_objective - - third_person_omniscient - storybeats: - type: array - items: - type: object - properties: - sequence: - type: integer - storybeat_id: - "$ref": "#/$defs/stable_id" - required: - - sequence - - storybeat_id - additionalProperties: false - required: - - summary - - synopsis - - setting - - timing - - imperatives - - storybeats - additionalProperties: false required: - overviews - - moments additionalProperties: false required: - id @@ -458,11 +396,19 @@ properties: - subtext - storytelling additionalProperties: false + moments: + type: array + description: Canonical story-level storytelling units. Moments should + live on story.moments[] so they can relate Storybeats and Storypoints + across one or more narratives. + items: + "$ref": "#/$defs/story_moment" required: - id - title - logline - created_at + - moments - narratives additionalProperties: false required: @@ -482,16 +428,20 @@ additionalProperties: false pattern: "^(?:story|narrative|beat)_[A-Za-z0-9][A-Za-z0-9_-]*$" perspective_id: type: string - description: Opaque identifier for a perspective. Plain UUIDs are fine; type prefixes are not required. + description: Opaque identifier for a perspective. Plain UUIDs are fine; type + prefixes are not required. player_id: type: string - description: Opaque identifier for a player. Plain UUIDs are fine; type prefixes are not required. + description: Opaque identifier for a player. Plain UUIDs are fine; type prefixes + are not required. overview_id: type: string - description: Opaque identifier for an overview. Plain UUIDs are fine; type prefixes are not required. + description: Opaque identifier for an overview. Plain UUIDs are fine; type + prefixes are not required. setting_id: type: string - description: Opaque identifier for a story-level setting. Plain UUIDs are fine; setting_ prefixes are optional. + description: Opaque identifier for a story-level setting. Plain UUIDs are + fine; setting_ prefixes are optional. perspective_link: type: object properties: @@ -1139,3 +1089,112 @@ additionalProperties: false - Work - Worry - Worth + story_moment: + type: object + properties: + id: + type: string + summary: + type: string + synopsis: + type: string + setting: + type: string + setting_id: + "$ref": "#/$defs/setting_id" + description: Optional reference to a story.settings[] entry for this + Moment. Use with setting to avoid repeating shared place details + while preserving the Moment-specific setting prose. + timing: + type: string + imperatives: + type: string + act: + type: integer + minimum: 1 + order: + type: integer + minimum: 0 + maximum_steps: + type: integer + minimum: 1 + fabric: + type: array + items: + type: object + properties: + type: + type: string + enum: + - space + - time + limit: + type: integer + required: + - type + - limit + additionalProperties: false + audience_experiential_pov: + type: string + enum: + - first_person_central + - first_person_peripheral + - second_person + - third_person_limited + - third_person_objective + - third_person_omniscient + storybeats: + type: array + items: + type: object + properties: + sequence: + type: integer + storybeat_id: + "$ref": "#/$defs/stable_id" + narrative_id: + type: string + description: The id of the narrative that owns the referenced + Storybeat. + required: + - sequence + - narrative_id + - storybeat_id + additionalProperties: false + description: Ordered references to narrative Storybeats touched by + this Moment. Each reference is qualified with narrative_id because + story-level Moments can span multiple narratives. + storypoints: + type: array + description: Ordered references to narrative Storypoints illustrated, + invoked, or turned by this Moment. Each reference is qualified + with narrative_id because story-level Moments can span multiple + narratives. + items: + type: object + properties: + sequence: + type: integer + narrative_id: + type: string + description: The id of the narrative that owns the referenced + Storypoint. + storypoint_id: + type: string + required: + - sequence + - narrative_id + - storypoint_id + additionalProperties: false + required: + - summary + - synopsis + - setting + - timing + - imperatives + - storybeats + - storypoints + additionalProperties: false + description: A story-level storytelling unit such as an act, scene, chapter, + sequence, level, or comparable audience-facing unit. Story-level Moments + can reference Storybeats and Storypoints from one or more narratives. diff --git a/tests/validate-schema.js b/tests/validate-schema.js index b2961ab..ca561d5 100644 --- a/tests/validate-schema.js +++ b/tests/validate-schema.js @@ -13,6 +13,7 @@ const validFixtures = [ '../examples/ideation-beginner.json', '../examples/complete-space-adventure-storyform.json', '../examples/complete-storyform-template.json', + '../examples/cross-narrative-moments.json', '../examples/storypoint-throughline-empty-perspectives.json', '../examples/storypoint-throughline-both-refs.json' ];