@miadi/ncp-story-studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/PHASES.md +160 -0
  3. package/README.md +140 -0
  4. package/assets/PROVENANCE.json +5 -0
  5. package/assets/examples/PROVENANCE.json +26 -0
  6. package/assets/examples/anora.json +228 -0
  7. package/assets/examples/example-story.json +112 -0
  8. package/assets/examples/the-shawshank-redemption.json +2068 -0
  9. package/assets/examples/weaver_of_words__the_catalyst_of_change_251101.json +1132 -0
  10. package/assets/ncp-schema.json +794 -0
  11. package/assets/terminology.json +2919 -0
  12. package/dist/agents/adapter.d.ts +60 -0
  13. package/dist/agents/adapter.d.ts.map +1 -0
  14. package/dist/agents/adapter.js +69 -0
  15. package/dist/agents/adapter.js.map +1 -0
  16. package/dist/agents/context.d.ts +19 -0
  17. package/dist/agents/context.d.ts.map +1 -0
  18. package/dist/agents/context.js +49 -0
  19. package/dist/agents/context.js.map +1 -0
  20. package/dist/agents/index.d.ts +5 -0
  21. package/dist/agents/index.d.ts.map +1 -0
  22. package/dist/agents/index.js +7 -0
  23. package/dist/agents/index.js.map +1 -0
  24. package/dist/agents/links.d.ts +41 -0
  25. package/dist/agents/links.d.ts.map +1 -0
  26. package/dist/agents/links.js +76 -0
  27. package/dist/agents/links.js.map +1 -0
  28. package/dist/agents/personas.d.ts +16 -0
  29. package/dist/agents/personas.d.ts.map +1 -0
  30. package/dist/agents/personas.js +34 -0
  31. package/dist/agents/personas.js.map +1 -0
  32. package/dist/fixtures/index.d.ts +27 -0
  33. package/dist/fixtures/index.d.ts.map +1 -0
  34. package/dist/fixtures/index.js +69 -0
  35. package/dist/fixtures/index.js.map +1 -0
  36. package/dist/graph/index.d.ts +60 -0
  37. package/dist/graph/index.d.ts.map +1 -0
  38. package/dist/graph/index.js +97 -0
  39. package/dist/graph/index.js.map +1 -0
  40. package/dist/graph/react/index.d.ts +2 -0
  41. package/dist/graph/react/index.d.ts.map +1 -0
  42. package/dist/graph/react/index.js +5 -0
  43. package/dist/graph/react/index.js.map +1 -0
  44. package/dist/graph/react/story-graph.d.ts +17 -0
  45. package/dist/graph/react/story-graph.d.ts.map +1 -0
  46. package/dist/graph/react/story-graph.js +26 -0
  47. package/dist/graph/react/story-graph.js.map +1 -0
  48. package/dist/graph/tones.d.ts +36 -0
  49. package/dist/graph/tones.d.ts.map +1 -0
  50. package/dist/graph/tones.js +38 -0
  51. package/dist/graph/tones.js.map +1 -0
  52. package/dist/index.d.ts +33 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +24 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/schema/constants.d.ts +3 -0
  57. package/dist/schema/constants.d.ts.map +1 -0
  58. package/dist/schema/constants.js +3 -0
  59. package/dist/schema/constants.js.map +1 -0
  60. package/dist/schema/extensions.d.ts +14 -0
  61. package/dist/schema/extensions.d.ts.map +1 -0
  62. package/dist/schema/extensions.js +61 -0
  63. package/dist/schema/extensions.js.map +1 -0
  64. package/dist/schema/generated-terms.d.ts +7 -0
  65. package/dist/schema/generated-terms.d.ts.map +1 -0
  66. package/dist/schema/generated-terms.js +518 -0
  67. package/dist/schema/generated-terms.js.map +1 -0
  68. package/dist/schema/generated.d.ts +64 -0
  69. package/dist/schema/generated.d.ts.map +1 -0
  70. package/dist/schema/generated.js +497 -0
  71. package/dist/schema/generated.js.map +1 -0
  72. package/dist/schema/index.d.ts +48 -0
  73. package/dist/schema/index.d.ts.map +1 -0
  74. package/dist/schema/index.js +92 -0
  75. package/dist/schema/index.js.map +1 -0
  76. package/dist/schema/normalize.d.ts +20 -0
  77. package/dist/schema/normalize.d.ts.map +1 -0
  78. package/dist/schema/normalize.js +88 -0
  79. package/dist/schema/normalize.js.map +1 -0
  80. package/dist/schema/read.d.ts +31 -0
  81. package/dist/schema/read.d.ts.map +1 -0
  82. package/dist/schema/read.js +640 -0
  83. package/dist/schema/read.js.map +1 -0
  84. package/dist/schema/terms.d.ts +11 -0
  85. package/dist/schema/terms.d.ts.map +1 -0
  86. package/dist/schema/terms.js +80 -0
  87. package/dist/schema/terms.js.map +1 -0
  88. package/dist/schema/types.d.ts +140 -0
  89. package/dist/schema/types.d.ts.map +1 -0
  90. package/dist/schema/types.js +2 -0
  91. package/dist/schema/types.js.map +1 -0
  92. package/dist/schema/validate.d.ts +54 -0
  93. package/dist/schema/validate.d.ts.map +1 -0
  94. package/dist/schema/validate.js +490 -0
  95. package/dist/schema/validate.js.map +1 -0
  96. package/dist/store/index.d.ts +76 -0
  97. package/dist/store/index.d.ts.map +1 -0
  98. package/dist/store/index.js +96 -0
  99. package/dist/store/index.js.map +1 -0
  100. package/dist/store/json.d.ts +35 -0
  101. package/dist/store/json.d.ts.map +1 -0
  102. package/dist/store/json.js +192 -0
  103. package/dist/store/json.js.map +1 -0
  104. package/dist/store/mutations.d.ts +45 -0
  105. package/dist/store/mutations.d.ts.map +1 -0
  106. package/dist/store/mutations.js +181 -0
  107. package/dist/store/mutations.js.map +1 -0
  108. package/dist/store/neon.d.ts +42 -0
  109. package/dist/store/neon.d.ts.map +1 -0
  110. package/dist/store/neon.js +167 -0
  111. package/dist/store/neon.js.map +1 -0
  112. package/dist/terminology/generated.d.ts +3 -0
  113. package/dist/terminology/generated.d.ts.map +1 -0
  114. package/dist/terminology/generated.js +2923 -0
  115. package/dist/terminology/generated.js.map +1 -0
  116. package/dist/terminology/index.d.ts +20 -0
  117. package/dist/terminology/index.d.ts.map +1 -0
  118. package/dist/terminology/index.js +71 -0
  119. package/dist/terminology/index.js.map +1 -0
  120. package/dist/terminology/types.d.ts +25 -0
  121. package/dist/terminology/types.d.ts.map +1 -0
  122. package/dist/terminology/types.js +2 -0
  123. package/dist/terminology/types.js.map +1 -0
  124. package/package.json +117 -0
  125. package/rispecs/README.rise.md +5 -0
  126. package/rispecs/RISPEC.md +9 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PHASES.md ADDED
@@ -0,0 +1,160 @@
1
+ # @miadi/ncp-story-studio — Phase Plan
2
+
3
+ **Written:** 2026-09-12 · steered by Guillaume's annotations on the capability survey.
4
+ **Reads with:** `AGENTS.md` (direction, version table stale — read manifests), `STATUS.md`,
5
+ `rispecs/`, and `/a/src/Miadi-18/rispecs/wampum-narrative-engine/` (later phases).
6
+ **Origin issue:** `jgwill/Miadi#361`. Wampum lanes: `#437` foundations, `#438` NCP bridge,
7
+ `#439` package surface.
8
+
9
+ ## Sources of truth
10
+
11
+ | what | where |
12
+ |---|---|
13
+ | Canonical NCP schema | `/workspace/repos/avadisabelle/holisticagnostic-narrative-context-protocol/schema/ncp-schema.json` |
14
+ | Prototype experience to rebuild | `/workspace/repos/miadisabelle/a1f8409e-1a36-495e-9ac2-84f617e6447c-ncp-story-studio-251125` |
15
+ | Governance research | that repo's `foundations/{decolonization-protocol-extensions,oral-tradition-storywork,linguistic-relativity-indigenous-epistemology}/` |
16
+ | Wampum specs | `rispecs/wampum-narrative-engine/` in this checkout |
17
+ | Consumption target | `app/ncp` in this checkout — **planned, not yet created** |
18
+
19
+ `app/ncp` is where the published packages are consumed to rebuild the prototype experience
20
+ inside Miadi, with the seed projects (Catalyst of Changes and the other four narratives)
21
+ observable there. It is a target, not a stale reference.
22
+
23
+ ## Status — 2026-09-12
24
+
25
+ Phase 1 and Phase 2 are **built and running**: `@miadi/ncp-story-studio` 0.1.0 with 86 tests, and
26
+ `app/ncp` consuming it on port 3335. What landed, and what each phase still holds, is below; the
27
+ module surface is in `README.md`.
28
+
29
+ Verified end to end on 2026-09-12: the four bundled examples seeded into a JSON store, the studio
30
+ rendering graph and beat editor, an edit saved from the browser through a server action to
31
+ `story.ncp.json` on disk, a conditional `PUT` accepted and a stale one refused with 409, an
32
+ unauthenticated write refused with 401.
33
+
34
+ ## Phase 1 — vocabulary and data · **built**
35
+
36
+ Goal: one NCP type surface everyone can produce compatible data against, and a viewer in
37
+ `app/ncp` reading real stories.
38
+
39
+ - **S1 `/schema`** — TypeScript types, a zero-dependency validator (Zod was dropped: a published
40
+ package should not pin a validation library on its consumers), the canonical JSON Schema as an
41
+ asset, and fixtures, from the
42
+ canonical schema (29 appreciations, 148/144 narrative functions, 9 dynamics × 16 vectors,
43
+ 3-axis tones, 6 audience POVs, `moments.fabric`). Prototype-only fields
44
+ (`custom_appreciation`, `custom_narrative_function`, `Player.motivations`,
45
+ `Perspective.throughline`) become **declared extensions**; `pov` renames to
46
+ `author_structural_pov`. No Wampum fields in the model at this phase.
47
+ - **S2 `/graph`** — the story lattice as a real component package (headless layout + React
48
+ renderer), consumed by `app/ncp`, not copied into it.
49
+ - **S9 examples + terminology** — the 5 canonical examples, the 5 prototype narratives, and
50
+ `docs/terminology/` as data behind a browser view. Teaching surface is first-class, not a
51
+ by-product.
52
+ - **`app/ncp` v1** — project list, story viewer, graph view, reading from the store provider.
53
+
54
+ ## Phase 2 — editing and story projects · **built**
55
+
56
+ - **S3 `/editor`** — CRUD for beats / players / storypoints, drag-reorder, dynamic perspectives,
57
+ live validation with structural-tension alerts. **Two storage providers behind one
58
+ interface: JSON files and Neon (Postgres).** Story projects are managed, not held in a tab.
59
+ - **S4 `/agents`** — the `[[Beat|Player|Perspective|Point:ID|Label]]` link protocol as its own
60
+ parser module, and a provider-neutral model adapter (the prototype hard-codes one vendor and
61
+ one model string). Personas: 🧠 Mia structural, 🌸 Miette narrative, 🎨 Ava8 musical.
62
+ - **S12 chronicle ↔ story** — import episode material into a story, and surface a story inside
63
+ an episode. Both directions, reusing `@miadi/inquiry-weave` and the chronicle readers in
64
+ `app/chronicle/lib/`.
65
+ - **S10 import / export** — NCP JSON in and out, then reuse what Miadi already publishes:
66
+ `@miadi/voice` for spoken readings, the Twine promotion path for a readable book,
67
+ `@miadi/musical-composition-to-episode` for composition links.
68
+
69
+ ### What Phase 1 and 2 taught, and what changed because of it
70
+
71
+ - **Three dialects, not one.** The canonical schema, the subtxt exports, and the Catalyst design
72
+ file disagree about shape *and* vocabulary. One reader now recognizes each and reports every
73
+ adaptation, which is what lets all four examples open in the same studio.
74
+ - **The canonical `required` lists name every field.** No shipped example satisfies them, so the
75
+ studio validates what a studio needs by default and enforces the schema's own lists on request.
76
+ - **Beat `sequence` numbers a series, not a narrative.** Signposts 1-4 and progressions 1-16 are
77
+ separate runs inside one throughline; uniqueness is checked per series.
78
+ - **An adversarial review found 18 defects** in the first cut, the worst of which deleted a store
79
+ root. Every one is fixed with a named regression test in `test/review-findings.test.mjs`.
80
+
81
+ ## Phase 3 — relational and symphonic readings
82
+
83
+ - **S11 Ava8 × NCP** — Ava8 is a musical orchestra agent, not a name collision. Relate story
84
+ `moments` to musical structure and explore symphonic capability inside a story. RISE specs
85
+ drafted during Phase 1–2 so this phase has ground to stand on.
86
+ - **S5 Wampum** — specs first (`rispecs/wampum-narrative-engine/` already holds the belt model,
87
+ the `project`/`read` bridge with its `crossing_report`, and the four-layer perspective
88
+ prompt). First concrete artifact: **Catalyst of Changes rendered in belt form**, to look at
89
+ and reflect on, before any schema support lands.
90
+ - **S6 governance, first implementations** — only the layers earlier phases proved they need.
91
+
92
+ ## Phase 4 — narrative workflow runtime
93
+
94
+ **S7** — goals, candidate trajectories, state versions, budgets, termination authority. The
95
+ spec needs revision before it is built; it is not Phase 1 material.
96
+
97
+ ## Phase 5 — three universes
98
+
99
+ **S8** — Engineer / Ceremony / Story validators returning evidence rather than a score.
100
+ Deliberately last: elements of the model resemble it already, and introducing it early would
101
+ confuse the vocabulary. It gets integrated where it belongs, not early.
102
+
103
+ ## The spec lane that runs through every phase
104
+
105
+ Specifications advance even where implementation does not.
106
+
107
+ - `rispecs/governance/<layer>/` — one folder per layer: transmission conditions, relational
108
+ provenance, linguistic frame, performativity, semantic access control. Each states who the
109
+ teller and receiver requirements are, what the layer means, and how it sits in the model.
110
+ Kept alive as later phases reveal what governance actually has to do. If a phase needs part
111
+ of it early, that part gets analysed and implemented then — not pre-built.
112
+ - Wampum specs and the Ava8 × NCP spec, written ahead of their phase for the same reason.
113
+
114
+ ## Consumption inside `app/`
115
+
116
+ - **C1** — publish order: schema, then the graph component, then editor, then agents. Headless
117
+ first: validators and layout carry no React so route handlers can use them; the React
118
+ renderer ships behind a subpath export (`@miadi/attention-ui` is the local precedent).
119
+ - **C2** — `workspace:*` in the root `package.json` while exports move; published semver once
120
+ they settle. Every manifest change updates `pnpm-lock.yaml` with scoped install/build/test.
121
+ - **C3** — surface: `app/ncp` (projects), `app/ncp/studio/[storyId]` (the studio),
122
+ `app/api/ncp/*` (validate, stories, later projections). Pattern to copy:
123
+ `app/chronicle/lib/*.ts` and `app/chronicle/[episode]/page.tsx` importing
124
+ `@miadi/episodic-memory-schema`, `@miadi/inquiry-weave`, `@miadi/tide` server-side.
125
+ - **C4** — validation runs server-side, where its record can be persisted.
126
+ - **C5** — storage provider chosen by environment: JSON files under a stories root, or Neon.
127
+ Browser storage only for per-viewer conveniences.
128
+ - **C6** — `app/live-story-monitor` and `app/narrative-lattice` become consumers of these
129
+ packages. Today `app/narrative-lattice/page.tsx` runs on fixtures from
130
+ `@/types/live-story-monitor-data`; they need feeding from the same schema and provider.
131
+ - **C7** — port 3335 is the single surface: dev mode while building, prod rebuild when
132
+ shipping, and state what is shipping from an untracked tree before rebuilding.
133
+
134
+ ## What this gives Concordia
135
+
136
+ - **K1** — an authorship record a rights or ethics review can read: state version, agent and
137
+ role, model, prompt, schema version, approvals, rejected alternatives.
138
+ - **K2** — per-field artifacts for the faculty alignment open as `jgwill/Miadi#611`–`#614`.
139
+ - **K3** — the Martin Lefebvre report in this folder is a literature anchor for the
140
+ narrative-theory framing. Cite from it; do not specify from it.
141
+ - **K4** — a seminar-ready teaching surface: terminology browser and worked example stories,
142
+ with governance handling stated in the data rather than promised in prose.
143
+
144
+ ## Risks that set the order
145
+
146
+ - **R2** — schema divergence between the prototype types and the canonical schema. Freeze S1
147
+ before porting any UI.
148
+ - **R3** — the schema repo is not vendored here (its only submodule is `llms`). Pin it or copy
149
+ the schema with a version stamp, or the enums drift silently.
150
+ - **R1** — cultural protocol. Deferring Wampum out of Phase 1 removes this from the critical
151
+ path. When it returns: public seed belts only, no community-specific readings without
152
+ consent, and the refusal gate exists before any generated reading ships.
153
+
154
+ ## Stale claims in `AGENTS.md` (do not carry forward)
155
+
156
+ - The version table. Real versions today: `@miadi/episodic-memory-schema` 0.10.0, `@miadi/tide`
157
+ 0.4.0, `@miadi/tide-contract` 0.3.0, `@miadi/hooks-core` 0.5.0, `@miadi/a2a-contracts` 0.1.1.
158
+ Read the manifests, per the `miadi-package-truth` skill.
159
+ - `pnpm add @miadi/ncp-story-studio` — no manifest, nothing published yet.
160
+ - `@miadi/three-universe-processor` appears in issue prose only; no such package exists.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @miadi/ncp-story-studio
2
+
3
+ The Narrative Context Protocol story studio, as consumable modules: canonical NCP types and a
4
+ zero-dependency validator, a reader that accepts the NCP dialects that actually exist, a headless
5
+ story-lattice layout with a React renderer, story-project storage over JSON files or Neon Postgres,
6
+ the agent link protocol, and the canonical terminology as data.
7
+
8
+ Phase plan, sources of truth, and what is deliberately not built yet: [`PHASES.md`](./PHASES.md).
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add @miadi/ncp-story-studio
14
+ # in this workspace
15
+ pnpm -w add @miadi/ncp-story-studio@workspace:* --filter <consumer>
16
+ ```
17
+
18
+ `react` and `@neondatabase/serverless` are optional peer dependencies: they are needed only by
19
+ `/graph/react` and `/store/neon`.
20
+
21
+ ## Modules
22
+
23
+ | Subpath | Needs | What it is |
24
+ |---|---|---|
25
+ | `@miadi/ncp-story-studio` | — | Package identity plus the schema surface |
26
+ | `.../schema` | — | Types, enums, validator, dialect reader, completeness |
27
+ | `.../graph` | — | Story lattice geometry, tone palette, SVG output |
28
+ | `.../graph/react` | react | `<StoryGraph>` — a client component |
29
+ | `.../store` | — | Story projects, pure edits, the provider contract |
30
+ | `.../store/json` | node | JSON files, one folder per project |
31
+ | `.../store/neon` | node, `@neondatabase/serverless` | Postgres, one row per project |
32
+ | `.../agents` | — | Personas, lattice context, the `[[Beat:id\|label]]` link protocol |
33
+ | `.../terminology` | — | The canonical NCP vocabulary as data |
34
+ | `.../fixtures` | node | The bundled example documents |
35
+
36
+ ## Schema
37
+
38
+ ```ts
39
+ import { readNcp, validateNcpDocument, storyCompleteness } from '@miadi/ncp-story-studio/schema'
40
+
41
+ const { document, dialect, notes } = readNcp(JSON.parse(raw))
42
+ const result = validateNcpDocument(document)
43
+
44
+ result.valid // no issue of severity `error`
45
+ result.issues // { path, code, severity, message } — evidence, not a score
46
+ result.extensions_used// which declared extensions this document relies on
47
+ storyCompleteness(document.story).overall // 0..1, measured against named targets
48
+ ```
49
+
50
+ ### Three dialects, one reader
51
+
52
+ NCP files in circulation are not all shaped the same way, and none of the differences is a defect —
53
+ they are what different tools wrote. `readNcp` recognizes each and reports every adaptation it makes.
54
+
55
+ | Dialect | Shape | Example |
56
+ |---|---|---|
57
+ | `canonical` | `{ schema_version, story { narratives[…] } }` | `example-story.json` |
58
+ | `subtxt-export` | `narratives` beside `story`, perspectives keyed by point of view, `context` strings, Dramatica display labels, placeholder ids | `the-shawshank-redemption.json`, `anora.json` |
59
+ | `miadi-design` | NCP container names with a design vocabulary (`label`, `description`, `event`, `structural_function`) | `weaver_of_words__the_catalyst_of_change_251101.json` |
60
+
61
+ Display labels ("Story Goal", "Success", "Main Character Resolve") are mapped to the canonical
62
+ identifiers through the protocol's own terminology data; whatever cannot be mapped is declared in a
63
+ `custom_*` field rather than silently dropped.
64
+
65
+ ### Declared extensions
66
+
67
+ Fields this workspace adds beyond the canonical schema are declared, not drifted:
68
+ `perspective.throughline`, `player.motivations`, `storypoint.custom_appreciation`,
69
+ `storypoint.custom_narrative_function`, `storybeat.custom_narrative_function`,
70
+ `dynamic.custom_dynamic`, `dynamic.custom_vector`. `validateNcpDocument` reports each one in use, and
71
+ `{ allowExtensions: false }` turns their use into an error.
72
+
73
+ ### Two strictness levels
74
+
75
+ By default the validator checks what a studio needs: identity, referential integrity, enum
76
+ membership, and per-series sequence uniqueness. `{ requireCanonicalFields: true }` adds the canonical
77
+ schema's own `required` lists — which name every field, including `visual`, `audio`, `illustration`
78
+ and `tones`. None of the shipped canonical examples satisfies that, which is why it is opt-in.
79
+
80
+ ## Graph
81
+
82
+ ```ts
83
+ import { layoutNarrative, layoutToSvg } from '@miadi/ncp-story-studio/graph'
84
+ const layout = layoutNarrative(narrative, { beatSpacing: 320 })
85
+ const svg = layoutToSvg(layout) // server-rendered, no DOM
86
+ ```
87
+
88
+ ```tsx
89
+ import { StoryGraph } from '@miadi/ncp-story-studio/graph/react'
90
+ <StoryGraph narrative={narrative} focusedBeatId={id} onBeatClick={setId} />
91
+ ```
92
+
93
+ ## Store
94
+
95
+ ```ts
96
+ import { JsonStoryStore } from '@miadi/ncp-story-studio/store/json'
97
+ import { NeonStoryStore } from '@miadi/ncp-story-studio/store/neon'
98
+
99
+ const store = new JsonStoryStore({ root: '/srv/miadi/ncp-story-projects' })
100
+ await store.init()
101
+ const project = await store.get('catalyst-of-change')
102
+ await store.save({ ...project, document }, { expected_updated_at: project.updated_at })
103
+ ```
104
+
105
+ `expected_updated_at` makes a write conditional. On Neon the guard is part of the statement, so two
106
+ concurrent writers cannot both succeed. On the filesystem it is enforced within one process and is
107
+ best-effort across processes — a filesystem has no atomic compare-and-set.
108
+
109
+ The JSON provider keeps the document as a plain NCP file (`story.ncp.json`) beside its metadata
110
+ (`project.json`), so exporting a project is copying it.
111
+
112
+ Story edits (`updateStorybeat`, `reorderStorybeats`, `upsertPlayer`, …) are pure: they return a new
113
+ story and never mutate the one they are given, so undo is keeping the previous value.
114
+
115
+ ## Agents
116
+
117
+ ```ts
118
+ import { runAgentTurn, parseAgentMessage } from '@miadi/ncp-story-studio/agents'
119
+ const response = await runAgentTurn(model, { story, message })
120
+ response.segments // text and links, ready to render
121
+ response.links // every link with whether it resolves in this story
122
+ response.provenance// which model answered, and how the story was carried
123
+ ```
124
+
125
+ `AgentModel` is an interface with one method, so the package depends on no vendor SDK. Personas:
126
+ 🧠 Mia reads structure, 🌸 Miette reads meaning, 🎨 Ava8 reads tone and musical shape.
127
+
128
+ ## Where the canonical data comes from
129
+
130
+ `assets/ncp-schema.json`, `assets/terminology.json` and `assets/examples/*.json` are copied from the
131
+ [Narrative Context Protocol](https://github.com/avadisabelle/holisticagnostic-narrative-context-protocol)
132
+ repository (MIT). They are re-synced, with a source path and sha256 stamped into the generated files:
133
+
134
+ ```bash
135
+ npm run sync # sync-schema + sync-docs
136
+ npm test # build, then node --test
137
+ ```
138
+
139
+ This package is a consumer of that protocol, never its author. When the schema changes there, the
140
+ generated enums, required lists, and term maps change here — and the stamp says which version is in.
@@ -0,0 +1,5 @@
1
+ {
2
+ "source_path": "/workspace/repos/avadisabelle/holisticagnostic-narrative-context-protocol/schema/ncp-schema.json",
3
+ "sha256": "c7a72b1588c988a063d6b92f9ee227f48885fcec666387e464843c434550d811",
4
+ "synced_at": "2026-09-12T06:02:32.378Z"
5
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "source_repo": "/workspace/repos/avadisabelle/holisticagnostic-narrative-context-protocol",
3
+ "synced_at": "2026-09-12T05:56:04.165Z",
4
+ "files": [
5
+ {
6
+ "file": "anora.json",
7
+ "bytes": 20969,
8
+ "sha256": "3a6e69c8ee4f0ddc36161bc785e88f2f89ae70e29d20e23fb08388e41990df89"
9
+ },
10
+ {
11
+ "file": "example-story.json",
12
+ "bytes": 5959,
13
+ "sha256": "5c3561246859b90d9107e29a6f064b5e8fcac4fdfa64376f0d630022e5ac6c95"
14
+ },
15
+ {
16
+ "file": "the-shawshank-redemption.json",
17
+ "bytes": 195730,
18
+ "sha256": "b2e9de432baf534d207e0a20c48c1e6675f64a8d0212736b4b7ea39836495358"
19
+ },
20
+ {
21
+ "file": "weaver_of_words__the_catalyst_of_change_251101.json",
22
+ "bytes": 66162,
23
+ "sha256": "5f442438e44c80b109a87a6d4f3d1ffcaf7079d086235a079031b789626f7ff5"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,228 @@
1
+ {
2
+ "story": {
3
+ "id": "af3b3394-1405-4236-befc-b8c869ef6059",
4
+ "title": "Anora",
5
+ "genre": "the genre",
6
+ "logline": "the logline",
7
+ "created_at": "2025-01-03T20:55:13.000000Z"
8
+ },
9
+ "narratives": [
10
+ {
11
+ "id": "7904027e-291a-44cf-a46f-4a6305894601",
12
+ "title": "Anora",
13
+ "subtext": {
14
+ "perspectives": {
15
+ "they": [
16
+ {
17
+ "id": "de74d6c4-7828-48fd-904f-b945ee2c5923",
18
+ "author_structural_pov": "they",
19
+ "summary": "Navigating the high-stakes world of Russian-oligarchy family dynamics",
20
+ "storytelling": "The story revolves around Ani's whirlwind journey through the dangerous, high-stakes world of the Zakharov family. Vanya's reckless immaturity and the family's power plays collide with Ani's determination to carve out her own survival in the face of manipulation, threats, and violence. The conflict escalates as Ani fights to maintain her independence amidst the chaos of annulments, bribes, and emotional betrayals."
21
+ }
22
+ ],
23
+ "i": [
24
+ {
25
+ "id": "c6d78240-f6dd-4b6f-8157-387a0fc26704",
26
+ "author_structural_pov": "i",
27
+ "summary": "Ani",
28
+ "storytelling": "Ani is a young woman struggling to survive in a world that sees her as disposable. As she navigates her way through stripping, eloping with a reckless rich boy, and facing the power of his family, Ani confronts her own hopes and fears for the future. Her fight is grounded in the desire to be seen as more than just a commodity, but as a person with value and agency."
29
+ }
30
+ ],
31
+ "you": [
32
+ {
33
+ "id": "2095e21f-1cdf-495f-a26d-ff2f084032e0",
34
+ "author_structural_pov": "you",
35
+ "summary": "Igor",
36
+ "storytelling": "Igor, a loyal enforcer for the Zakharov family, embodies a chilling mix of pragmatism and intimidation. He challenges Ani\u00e2\u20ac\u2122s sense of agency with his cold demeanor and violent methods but also reveals unexpected moments of humanity that complicate her perception of him. Igor forces Ani to confront the harsh realities of survival in the Zakharov family's world."
37
+ }
38
+ ],
39
+ "we": [
40
+ {
41
+ "id": "0444eba4-a912-4ac9-9b2d-c1287aec01d1",
42
+ "author_structural_pov": "we",
43
+ "summary": "Protector/Protectee",
44
+ "storytelling": "The bond between Ani and Igor evolves from distrust and aggression to a complicated dynamic of reluctant understanding. As the protector and the protectee, their relationship is fraught with tension, power imbalances, and moments of vulnerability. Ani and Igor\u00e2\u20ac\u2122s interactions explore the fine line between control and care, forcing both to confront their own prejudices and vulnerabilities."
45
+ }
46
+ ]
47
+ },
48
+ "players": [
49
+ {
50
+ "id": "812f9f71-29b6-49e0-983a-60ecce5809cc",
51
+ "name": "Anora \"Ani\" Mikheeva",
52
+ "role": "a high-priced stripper at the Headquarters strip club",
53
+ "visual": "",
54
+ "audio": "",
55
+ "bio": "",
56
+ "storytelling": "",
57
+ "elements": [
58
+ {
59
+ "element": "Uncontrolled",
60
+ "storytelling": ""
61
+ }
62
+ ],
63
+ "perspectives": {
64
+ "perspective_id": "d80805bf-073b-4ea5-985c-ffa16e697d84"
65
+ }
66
+ },
67
+ {
68
+ "id": "aee134eb-a88e-4f5e-8aec-ec5c60b7006e",
69
+ "name": "Igor",
70
+ "role": "a Russian henchman hired by Toros to look after Vanya",
71
+ "visual": "",
72
+ "audio": "",
73
+ "bio": "",
74
+ "storytelling": "",
75
+ "elements": [
76
+ {
77
+ "element": "Control",
78
+ "storytelling": ""
79
+ }
80
+ ],
81
+ "perspectives": {
82
+ "perspective_id": "3fc05215-ab5c-4c1a-84d8-9a3dd672cb53"
83
+ }
84
+ }
85
+ ],
86
+ "storypoints": [
87
+ {
88
+ "id": "7b7a431e-0f5a-4a3e-a92d-2a22e4a5f23d",
89
+ "context": "Premise",
90
+ "appreciation": "Subjective Premise",
91
+ "narrative_function": "Uncontrolled",
92
+ "illustration": "being in a disorganized group",
93
+ "summary": "",
94
+ "storytelling": "",
95
+ "perspectives": {
96
+ "perspective_id": "d2a170ac-4cf5-49d3-a770-761238800f64"
97
+ }
98
+ },
99
+ {
100
+ "id": "b0112475-0156-4778-a19a-e1dc271a4612",
101
+ "context": "Premise",
102
+ "appreciation": "Objective Premise",
103
+ "narrative_function": "Self Interest",
104
+ "illustration": "thinking about one's own interests",
105
+ "summary": "",
106
+ "storytelling": "",
107
+ "perspectives": {
108
+ "perspective_id": "89706d68-c599-4846-a821-e39d3c05753d"
109
+ }
110
+ },
111
+ {
112
+ "id": "ea9670d5-02fb-4199-8f6d-f7d8460d90fd",
113
+ "context": "Premise",
114
+ "appreciation": "Subjective Premise Balance",
115
+ "narrative_function": "Control",
116
+ "illustration": "living controlled by attachments",
117
+ "summary": "",
118
+ "storytelling": "",
119
+ "perspectives": {
120
+ "perspective_id": "be676cc8-2f9c-45b0-b57a-5f91691f676d"
121
+ }
122
+ },
123
+ {
124
+ "id": "a762d2bf-73e7-46cf-9980-4d1e18f2c6c5",
125
+ "context": "Objective Story",
126
+ "appreciation": "Story Goal",
127
+ "narrative_function": "Obtaining",
128
+ "illustration": "obtaining freedom",
129
+ "summary": "",
130
+ "storytelling": "The Story Goal of Obtaining manifests in Ani's relentless pursuit of agency and self-worth in a world that constantly seeks to strip her of both. Ani's journey is fraught with external and internal conflict as she battles the Zakharov family's manipulative power plays and Vanya's reckless immaturity, all while trying to secure her own survival and independence. Her struggle to \"obtain\" is not just about material survival\u00e2\u20ac\u201dthough that is a pressing concern\u00e2\u20ac\u201dbut about achieving recognition as a person of value in a world that commodifies her. This intention drives her to make bold, often dangerous choices, such as eloping with Vanya or standing up to the family's threats, even when the odds are stacked against her. Ani's growth is marked by her increasing ability to assert her own desires and boundaries, refusing to be a pawn in the Zakharov family's games. The tension between her need to obtain control over her life and the forces trying to dominate her creates a gripping narrative of resilience and self-determination.",
131
+ "perspectives": {
132
+ "perspective_id": "31fe11ad-9699-4b5f-86da-7fce127eb974"
133
+ }
134
+ },
135
+ {
136
+ "id": "6a9120b5-abbc-47be-9eb7-064dedafcca6",
137
+ "context": "Objective Story",
138
+ "appreciation": "Story Consequence",
139
+ "narrative_function": "Becoming",
140
+ "illustration": "becoming someone else",
141
+ "summary": "",
142
+ "storytelling": "The sensation of overwhelm in this story stems from Ani's struggle with \"Becoming\"\u00e2\u20ac\u201dthe pressure to transform into someone she is not in order to survive the Zakharov family's manipulative world. As Ani fights to obtain her freedom, she is constantly forced to adapt, shapeshift, and play roles dictated by the powerful forces around her. The overwhelming nature of this conflict manifests in moments where Ani feels herself losing her identity, such as when she must feign loyalty to Vanya despite his recklessness or when she is coerced into navigating the family's dangerous schemes. Each compromise Ani makes to stay alive and protect herself chips away at her sense of self, leaving her questioning who she is becoming in the process. The weight of these transformations\u00e2\u20ac\u201dbeing seen as a pawn, a trophy, or a threat\u00e2\u20ac\u201dclashes with her deep desire to be valued as an individual, creating a suffocating tension that drives her to the brink of emotional collapse. This internal battle between survival and selfhood defines her overwhelming journey.",
143
+ "perspectives": {
144
+ "perspective_id": "454be6be-413b-4b75-9488-528fdcdced82"
145
+ }
146
+ },
147
+ {
148
+ "id": "040258a2-b6d0-4f14-ad53-10c3ab02892f",
149
+ "context": "Main Character",
150
+ "appreciation": "Problem",
151
+ "narrative_function": "Control",
152
+ "illustration": "someone being controlled",
153
+ "summary": "",
154
+ "storytelling": "Ani's MC Condition of Control is reflected in her constant struggle to assert agency over her life in a world that seeks to dominate and define her. Her work as a stripper places her in an environment where others attempt to control her body and identity, while her relationship with the reckless rich boy introduces a new layer of manipulation as his family exerts their influence to maintain their power and status. Ani's journey is marked by her attempts to wrest control back from these external forces, often clashing with societal expectations and the oppressive systems around her. This condition also manifests internally, as Ani grapples with her own fears of losing control over her future and succumbing to the roles others impose on her. Her growth is driven by her ability to redefine control\u00e2\u20ac\u201dnot as domination, but as self-determination\u00e2\u20ac\u201dultimately finding strength in her own choices and reclaiming her sense of worth.",
155
+ "perspectives": {
156
+ "perspective_id": "1e4d10e2-9025-4015-ade6-7c42fcbe896d"
157
+ }
158
+ },
159
+ {
160
+ "id": "65bdce43-852e-4a5c-be4f-4ea623507257",
161
+ "context": "Main Character",
162
+ "appreciation": "Solution",
163
+ "narrative_function": "Uncontrolled",
164
+ "illustration": "abandoning",
165
+ "summary": "",
166
+ "storytelling": "Ani's journey revolves around her struggle with the Condition of Control, as she is constantly manipulated by societal expectations, the rich boy she elopes with, and the oppressive power of his family. At first, Ani fights to assert her agency within these constraints, believing she can wrest control of her life by playing within the rules of those who dominate her. However, her transformative Revision to Abandon occurs when she realizes that true freedom cannot be achieved by clinging to the systems and relationships that seek to control her.This shift is catalyzed by a moment of profound betrayal or disillusionment\u00e2\u20ac\u201dperhaps the rich boy's family uses her as a scapegoat for a scandal, or she discovers he views her as a trophy rather than an equal partner. In abandoning her need to control the narrative others have imposed on her, Ani chooses to walk away from the toxic dynamics entirely, even if it means starting over with nothing. This act of relinquishing control paradoxically grants her the agency she has been seeking, as she begins to define her worth on her own terms. By the end of the story, Ani finds a balance between self-reliance and vulnerability, embracing a life where she no longer seeks validation from those who once sought to dominate her.",
167
+ "perspectives": {
168
+ "perspective_id": "cee70951-e8fb-4147-9637-8dda6285d468"
169
+ }
170
+ },
171
+ {
172
+ "id": "8abcf5c1-1dce-49c4-ba4b-cac6611ed43a",
173
+ "context": "Influence Character",
174
+ "appreciation": "Problem",
175
+ "narrative_function": "Consider",
176
+ "illustration": "considering someone's options",
177
+ "summary": "",
178
+ "storytelling": "Igor\u00e2\u20ac\u2122s OC Condition of \"Consider\" manifests in his unyielding focus on the Zakharov family's interests, refusing to entertain any perspective outside of their survival and dominance. His cold pragmatism and violent methods force Ani to confront her own tendency to avoid hard truths about her circumstances. Igor\u00e2\u20ac\u2122s refusal to consider alternatives\u00e2\u20ac\u201dwhether it\u00e2\u20ac\u2122s sparing someone\u00e2\u20ac\u2122s life or deviating from orders\u00e2\u20ac\u201dcreates a stark contrast to Ani\u00e2\u20ac\u2122s growing desire for agency and self-worth. However, his rare moments of humanity, such as a fleeting act of mercy or a begrudging acknowledgment of Ani\u00e2\u20ac\u2122s resilience, challenge her to see the complexity in even the most brutal figures. This dynamic pushes Ani to reconcile her own survival instincts with her longing to be more than a pawn, ultimately driving her to claim her agency in a world that demands submission. Igor\u00e2\u20ac\u2122s unwavering focus on the Zakharov family\u00e2\u20ac\u2122s survival becomes the mirror through which Ani must decide what she is willing to fight for\u00e2\u20ac\u201dand at what cost.",
179
+ "perspectives": {
180
+ "perspective_id": "2c2a096a-e948-4da8-8e5c-51fd8d3a64ee"
181
+ }
182
+ },
183
+ {
184
+ "id": "11a36847-13c4-447e-ae7a-373eca54c5bb",
185
+ "context": "Influence Character",
186
+ "appreciation": "Evolution",
187
+ "narrative_function": "Consider",
188
+ "illustration": "being considerate",
189
+ "summary": "",
190
+ "storytelling": "Igor\u00e2\u20ac\u2122s reaffirmed motivation of \"Consider\" manifests in a pivotal moment where his unexpected humanity shines through, forcing Ani to reevaluate her perception of him and, by extension, her own approach to survival. Despite his violent role within the Zakharov family, Igor demonstrates a profound, albeit quiet, consideration for Ani\u00e2\u20ac\u2122s plight\u00e2\u20ac\u201dperhaps by subtly ensuring her safety during a dangerous encounter or offering her a piece of advice that could save her life. This act of consideration, while consistent with his original motivation, challenges Ani to see beyond her hardened view of the world and recognize that even in the darkest corners, there are shades of complexity. Igor\u00e2\u20ac\u2122s actions push Ani to confront her own agency, urging her to consider not just how she survives, but how she defines her worth and humanity in a world that seeks to strip it away.",
191
+ "perspectives": {
192
+ "perspective_id": "3be4c7ae-fc4b-4888-a6b3-9730f0306c58"
193
+ }
194
+ },
195
+ {
196
+ "id": "4d9d77a4-ea43-40b3-b3e1-253905156b08",
197
+ "context": "Main Character",
198
+ "appreciation": "Pivotal Element",
199
+ "narrative_function": "Uncontrolled",
200
+ "illustration": "being wild",
201
+ "summary": "Ani argues that embracing a \"wild and crazy\" lifestyle is a powerful act of reclaiming agency and individuality in a dehumanizing world, challenging societal expectations and asserting her worth.",
202
+ "storytelling": "Ani\u00e2\u20ac\u2122s argument centers on the idea that embracing a \"wild and crazy\" approach to life is sometimes the only way to assert one\u00e2\u20ac\u2122s humanity in a world that dehumanizes and commodifies people. She contends that by taking risks\u00e2\u20ac\u201dwhether it\u00e2\u20ac\u2122s stripping to survive, eloping with a reckless rich boy, or standing up to his powerful family\u00e2\u20ac\u201dshe is reclaiming her agency and refusing to be boxed into society\u00e2\u20ac\u2122s expectations. Ani argues that living boldly, even chaotically, is a form of resistance against a system that sees her as disposable. Her actions, though unconventional, are a declaration that she is more than a product of her circumstances; she is a person with dreams, fears, and the right to define her own worth.Through her journey, Ani demonstrates that being \"wild and crazy\" is not about recklessness for its own sake, but about refusing to conform to a world that tries to strip her of her individuality. She uses her audacity to challenge the power structures around her, forcing others to see her as a person rather than a commodity. In doing so, Ani makes a compelling case that sometimes, the only way to survive\u00e2\u20ac\u201dand thrive\u00e2\u20ac\u201dis to embrace the chaos and carve out a space where one\u00e2\u20ac\u2122s voice and value cannot be ignored.",
203
+ "perspectives": {
204
+ "perspective_id": "29ec7f82-057c-4df2-a429-1df8f2f7d609"
205
+ }
206
+ },
207
+ {
208
+ "id": "3f5008c7-fd22-4903-adeb-1219bc77092c",
209
+ "context": "Influence Character",
210
+ "appreciation": "Pivotal Element",
211
+ "narrative_function": "Control",
212
+ "illustration": "keeping a group under control",
213
+ "summary": "Igor illustrates that in the brutal world of the Zakharov family's criminal empire, being considerate is a strategic strength that fosters loyalty and survival, challenging Ani's views on morality and power.",
214
+ "storytelling": "Igor makes a compelling argument for the value of being considerate, even in a brutal and unforgiving world like the Zakharov family's criminal empire. He demonstrates that consideration is not a weakness but a calculated strength, one that can foster loyalty and ensure survival. For example, while his violent methods often leave Ani questioning her own agency, Igor occasionally shows unexpected kindness\u00e2\u20ac\u201dperhaps sparing a life when it serves no purpose to take it or offering Ani advice that helps her navigate the treacherous dynamics of their world. These moments of humanity reveal that even in a world ruled by power and fear, being considerate can be a tool for influence and control. Igor argues, through his actions, that understanding others' needs and vulnerabilities can be just as effective as brute force, forcing Ani to reevaluate her assumptions about morality and survival.",
215
+ "perspectives": {
216
+ "perspective_id": "d2d13bd3-f669-4968-872b-0f8d16704b0a"
217
+ }
218
+ }
219
+ ],
220
+ "storybeats": []
221
+ },
222
+ "storytelling": {
223
+ "overviews": [],
224
+ "moments": []
225
+ }
226
+ }
227
+ ]
228
+ }