diff --git a/HISTORY.md b/HISTORY.md index 059b4b4..23e1212 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,8 @@ ## 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`. +- 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. - Clarified that canonical Storybeat objects do not expose a `signpost` key; any internal grouping should be derived from structural scope, sequence, and parent relationships. diff --git a/SPECIFICATION.md b/SPECIFICATION.md index 81110ca..09edcf7 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -93,6 +93,22 @@ For open-source adopters, this creates a shared on-ramp: communities can exchang ``` +## Story Settings + +NCP supports optional reusable story-level Settings for places or environments that recur across Moments. `story.settings[]` defines the shared setting once, while each Moment keeps its own free-text `setting` and may optionally reference the shared entry with `setting_id`. + +This avoids repeating the same place details across multiple Moments without making every producer model setting identity. Producers that only need free-text Moment settings can omit `story.settings[]` and `setting_id`. + +```json +"settings": [ + { + "id": "setting_precinct_archive", + "name": "Precinct Archive", + "description": "A dimly lit archive room where old case files preserve institutional memory." + } +] +``` + ## Narrative: Structuring Subtext & Storytelling A single story may contain one or more narratives (e.g., _The Empire Strikes Back_ has the Luke/Yoda Storyform and the Han/Leia Storyform, _Barbie_ has the Barbie/Ken Storyform and the Barbie/Gloria Storyform). Most stories, however, exhibit a single central narrative (e.g., _Anora_, _Anatomy of a Fall_, etc.). @@ -414,6 +430,7 @@ Organizational narrative units—such as Acts, Scenes, Sequences, Chapters, and "summary": "Infiltrating the neon-lit heart of a dystopian metropolis, Alex plunges into a shadowy realm teeming with digital outlaws.", "synopsis": "Freshly arrived in the neon chaos of Neo-Tokyo, Alex is swiftly ensnared in a perilous game played by cyber-criminals, underground syndicates, and relentless AI-driven enforcers.", "setting": "The pulsating streets of Neo-Tokyo, where holographic ads blend with the shadowy back alleys controlled by syndicate bosses.", + "setting_id": "setting_neo_tokyo_streets", "timing": "Late night, just hours after Alex's first unsettling discovery upon arriving in the city.", "imperatives": "- Establish the dark, chaotic atmosphere of Neo-Tokyo\n- Introduce key threats: cyber-criminals and AI enforcers\n- Show Alex's initial vulnerabilities and resourcefulness", "audience_experiential_pov": "third_person_limited", diff --git a/docs/narrative-context-protocol-schema.md b/docs/narrative-context-protocol-schema.md index c7c53e3..c14e53e 100644 --- a/docs/narrative-context-protocol-schema.md +++ b/docs/narrative-context-protocol-schema.md @@ -104,6 +104,22 @@ Quick heuristic: - If it is an event chain or conflict progression, put it in `plot`. - If it is a framing/experience contract with the audience, put it in `genre`. +## Story Settings + +`story.settings[]` is an optional story-level glossary for reusable places or environments. Each setting requires an `id` and `name`, with an optional `description`. Moments may keep their local free-text `setting` prose while also pointing at a shared setting with `setting_id`. + +Use this when multiple Moments occur in the same place and producers want a stable reference instead of repeating or string-matching setting descriptions. + +```json +"settings": [ + { + "id": "setting_precinct_archive", + "name": "Precinct Archive", + "description": "A dimly lit archive room where old case files preserve institutional memory." + } +] +``` + ## Narrative Layers Each item in `story.narratives[]` is a Dramatica storyform: a single, complete argument structure within the story, expressed through `subtext` and `storytelling` layers. @@ -290,7 +306,9 @@ Required keys per item: Optional keys: -- `id`, `act`, `order`, `maximum_steps`, `fabric`, `audience_experiential_pov` +- `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: diff --git a/examples/anora.json b/examples/anora.json index a9c9e31..4ac04a3 100644 --- a/examples/anora.json +++ b/examples/anora.json @@ -169,7 +169,6 @@ "id": "beat_anora_signpost_1", "scope": "signpost", "sequence": 1, - "signpost": 1, "throughline": "Main Character", "narrative_function": "Present", "summary": "Ani experiences the immediate thrill and danger of rapid escalation.", @@ -184,7 +183,6 @@ "id": "beat_anora_progression_3", "scope": "progression", "sequence": 3, - "signpost": 1, "throughline": "Objective Story", "narrative_function": "Learning", "summary": "The family applies escalating leverage to force compliance.", diff --git a/examples/complete-storyform-template.json b/examples/complete-storyform-template.json index fb49cf4..f85836b 100644 --- a/examples/complete-storyform-template.json +++ b/examples/complete-storyform-template.json @@ -1191,7 +1191,6 @@ "id": "beat_objective_story_signpost_1", "scope": "signpost", "sequence": 1, - "signpost": 1, "throughline": "Objective Story", "summary": "", "storytelling": "", @@ -1205,7 +1204,6 @@ "id": "beat_objective_story_signpost_2", "scope": "signpost", "sequence": 2, - "signpost": 2, "throughline": "Objective Story", "summary": "", "storytelling": "", @@ -1219,7 +1217,6 @@ "id": "beat_objective_story_signpost_3", "scope": "signpost", "sequence": 3, - "signpost": 3, "throughline": "Objective Story", "summary": "", "storytelling": "", @@ -1233,7 +1230,6 @@ "id": "beat_objective_story_signpost_4", "scope": "signpost", "sequence": 4, - "signpost": 4, "throughline": "Objective Story", "summary": "", "storytelling": "", @@ -1247,7 +1243,6 @@ "id": "beat_main_character_signpost_1", "scope": "signpost", "sequence": 1, - "signpost": 1, "throughline": "Main Character", "summary": "", "storytelling": "", @@ -1261,7 +1256,6 @@ "id": "beat_main_character_signpost_2", "scope": "signpost", "sequence": 2, - "signpost": 2, "throughline": "Main Character", "summary": "", "storytelling": "", @@ -1275,7 +1269,6 @@ "id": "beat_main_character_signpost_3", "scope": "signpost", "sequence": 3, - "signpost": 3, "throughline": "Main Character", "summary": "", "storytelling": "", @@ -1289,7 +1282,6 @@ "id": "beat_main_character_signpost_4", "scope": "signpost", "sequence": 4, - "signpost": 4, "throughline": "Main Character", "summary": "", "storytelling": "", @@ -1303,7 +1295,6 @@ "id": "beat_influence_character_signpost_1", "scope": "signpost", "sequence": 1, - "signpost": 1, "throughline": "Influence Character", "summary": "", "storytelling": "", @@ -1317,7 +1308,6 @@ "id": "beat_influence_character_signpost_2", "scope": "signpost", "sequence": 2, - "signpost": 2, "throughline": "Influence Character", "summary": "", "storytelling": "", @@ -1331,7 +1321,6 @@ "id": "beat_influence_character_signpost_3", "scope": "signpost", "sequence": 3, - "signpost": 3, "throughline": "Influence Character", "summary": "", "storytelling": "", @@ -1345,7 +1334,6 @@ "id": "beat_influence_character_signpost_4", "scope": "signpost", "sequence": 4, - "signpost": 4, "throughline": "Influence Character", "summary": "", "storytelling": "", @@ -1359,7 +1347,6 @@ "id": "beat_relationship_story_signpost_1", "scope": "signpost", "sequence": 1, - "signpost": 1, "throughline": "Relationship Story", "summary": "", "storytelling": "", @@ -1373,7 +1360,6 @@ "id": "beat_relationship_story_signpost_2", "scope": "signpost", "sequence": 2, - "signpost": 2, "throughline": "Relationship Story", "summary": "", "storytelling": "", @@ -1387,7 +1373,6 @@ "id": "beat_relationship_story_signpost_3", "scope": "signpost", "sequence": 3, - "signpost": 3, "throughline": "Relationship Story", "summary": "", "storytelling": "", @@ -1401,7 +1386,6 @@ "id": "beat_relationship_story_signpost_4", "scope": "signpost", "sequence": 4, - "signpost": 4, "throughline": "Relationship Story", "summary": "", "storytelling": "", diff --git a/examples/example-story.json b/examples/example-story.json index 77a6a57..5444667 100644 --- a/examples/example-story.json +++ b/examples/example-story.json @@ -6,6 +6,13 @@ "genre": "Mystery Thriller", "logline": "A hardened detective uncovers clues linking a cold case to his own haunting history.", "created_at": "2025-12-01T12:34:56Z", + "settings": [ + { + "id": "setting_precinct_archive", + "name": "Precinct Archive", + "description": "A dimly lit archive room where old case files preserve institutional memory and unresolved history." + } + ], "ideation": { "character": [ { @@ -112,7 +119,6 @@ "id": "beat_001", "scope": "signpost", "sequence": 2, - "signpost": 2, "throughline": "Main Character", "narrative_function": "Preconscious", "summary": "A moment where the past resurfaces, influencing his current investigation.", @@ -149,6 +155,7 @@ "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": [ diff --git a/examples/story-settings.json b/examples/story-settings.json new file mode 100644 index 0000000..eb1748c --- /dev/null +++ b/examples/story-settings.json @@ -0,0 +1,44 @@ +{ + "schema_version": "1.3.0", + "story": { + "id": "story_settings_demo", + "title": "Shared Settings Demo", + "logline": "A detective returns to the same archive twice and discovers that the room itself carries meaning.", + "created_at": "2026-05-12T20:10:00Z", + "settings": [ + { + "id": "setting_precinct_archive", + "name": "Precinct Archive", + "description": "A dim archive room where unresolved cases gather institutional dust." + } + ], + "narratives": [ + { + "id": "narrative_settings_demo", + "title": "Central Narrative", + "subtext": { + "perspectives": [], + "players": [], + "dynamics": [], + "storypoints": [], + "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": [] + } + ] + } + } + ] + } +} diff --git a/examples/storypoint-throughline-both-refs.json b/examples/storypoint-throughline-both-refs.json index c280a15..7baa420 100644 --- a/examples/storypoint-throughline-both-refs.json +++ b/examples/storypoint-throughline-both-refs.json @@ -9,6 +9,7 @@ "narratives": [ { "id": "narrative_storypoint_throughline_with_perspective", + "title": "Throughline Perspective Narrative", "subtext": { "perspectives": [ { diff --git a/examples/storypoint-throughline-empty-perspectives.json b/examples/storypoint-throughline-empty-perspectives.json index 2a7d523..f7a2177 100644 --- a/examples/storypoint-throughline-empty-perspectives.json +++ b/examples/storypoint-throughline-empty-perspectives.json @@ -9,6 +9,7 @@ "narratives": [ { "id": "narrative_storypoint_throughline_empty_perspectives", + "title": "Throughline Placeholder Narrative", "subtext": { "perspectives": [], "players": [], diff --git a/examples/the-shawshank-redemption.json b/examples/the-shawshank-redemption.json index 06fc3fb..2c8ed0a 100644 --- a/examples/the-shawshank-redemption.json +++ b/examples/the-shawshank-redemption.json @@ -179,7 +179,6 @@ "id": "beat_shawshank_signpost_1", "scope": "signpost", "sequence": 1, - "signpost": 1, "throughline": "Objective Story", "narrative_function": "Past", "summary": "Past convictions and institutional history define present constraints.", @@ -194,7 +193,6 @@ "id": "beat_shawshank_progression_6", "scope": "progression", "sequence": 6, - "signpost": 2, "throughline": "Relationship Story", "narrative_function": "Trust", "summary": "Red and Andy move from caution toward mutual commitment.", diff --git a/schema/ncp-schema.json b/schema/ncp-schema.json index 364b615..edf1eff 100644 --- a/schema/ncp-schema.json +++ b/schema/ncp-schema.json @@ -29,6 +29,31 @@ "description": "ISO-8601 UTC timestamp (e.g., 2025-12-01T12:34:56Z).", "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.", + "items": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/setting_id" + }, + "name": { + "type": "string", + "description": "Short human-readable setting name." + }, + "description": { + "type": "string", + "description": "Optional longer setting description." + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + } + }, "ideation": { "type": "object", "description": "Optional pre-narrative ideation threads for exploratory and beginner workflows.", @@ -90,17 +115,17 @@ "subtext": { "type": "object", "properties": { - "perspectives": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "$ref": "#/$defs/perspective_id" - }, - "author_structural_pov": { - "type": "string", - "enum": [ + "perspectives": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/perspective_id" + }, + "author_structural_pov": { + "type": "string", + "enum": [ "i", "you", "we", @@ -348,17 +373,17 @@ "type": "array", "items": { "type": "object", - "properties": { - "id": { - "$ref": "#/$defs/stable_id" - }, - "appreciation": { - "type": "string", - "description": "Optional derived structural label, typically throughline + scope + sequence (for example, Objective Story Signpost 1)." - }, - "scope": { - "type": "string", - "enum": [ + "properties": { + "id": { + "$ref": "#/$defs/stable_id" + }, + "appreciation": { + "type": "string", + "description": "Optional derived structural label, typically throughline + scope + sequence (for example, Objective Story Signpost 1)." + }, + "scope": { + "type": "string", + "enum": [ "signpost", "progression", "event" @@ -515,6 +540,10 @@ "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" }, @@ -661,6 +690,10 @@ "type": "string", "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." + }, "perspective_link": { "type": "object", "properties": { diff --git a/schema/ncp-schema.yaml b/schema/ncp-schema.yaml index 73c5cea..512c497 100644 --- a/schema/ncp-schema.yaml +++ b/schema/ncp-schema.yaml @@ -22,6 +22,24 @@ properties: type: string description: ISO-8601 UTC timestamp (e.g., 2025-12-01T12:34:56Z). 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. + items: + type: object + properties: + id: + "$ref": "#/$defs/setting_id" + name: + type: string + description: Short human-readable setting name. + description: + type: string + description: Optional longer setting description. + required: + - id + - name + additionalProperties: false ideation: type: object description: Optional pre-narrative ideation threads for exploratory and beginner workflows. @@ -368,6 +386,9 @@ properties: 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: @@ -468,6 +489,9 @@ additionalProperties: false overview_id: type: string 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. perspective_link: type: object properties: diff --git a/tests/validate-schema.js b/tests/validate-schema.js index 298a925..97e14c7 100644 --- a/tests/validate-schema.js +++ b/tests/validate-schema.js @@ -9,6 +9,7 @@ const validate = ajv.compile(schema); const validFixtures = [ '../examples/example-story.json', + '../examples/story-settings.json', '../examples/ideation-beginner.json', '../examples/anora.json', '../examples/the-shawshank-redemption.json',