@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.
- package/LICENSE +21 -0
- package/PHASES.md +160 -0
- package/README.md +140 -0
- package/assets/PROVENANCE.json +5 -0
- package/assets/examples/PROVENANCE.json +26 -0
- package/assets/examples/anora.json +228 -0
- package/assets/examples/example-story.json +112 -0
- package/assets/examples/the-shawshank-redemption.json +2068 -0
- package/assets/examples/weaver_of_words__the_catalyst_of_change_251101.json +1132 -0
- package/assets/ncp-schema.json +794 -0
- package/assets/terminology.json +2919 -0
- package/dist/agents/adapter.d.ts +60 -0
- package/dist/agents/adapter.d.ts.map +1 -0
- package/dist/agents/adapter.js +69 -0
- package/dist/agents/adapter.js.map +1 -0
- package/dist/agents/context.d.ts +19 -0
- package/dist/agents/context.d.ts.map +1 -0
- package/dist/agents/context.js +49 -0
- package/dist/agents/context.js.map +1 -0
- package/dist/agents/index.d.ts +5 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +7 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/links.d.ts +41 -0
- package/dist/agents/links.d.ts.map +1 -0
- package/dist/agents/links.js +76 -0
- package/dist/agents/links.js.map +1 -0
- package/dist/agents/personas.d.ts +16 -0
- package/dist/agents/personas.d.ts.map +1 -0
- package/dist/agents/personas.js +34 -0
- package/dist/agents/personas.js.map +1 -0
- package/dist/fixtures/index.d.ts +27 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +69 -0
- package/dist/fixtures/index.js.map +1 -0
- package/dist/graph/index.d.ts +60 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +97 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/react/index.d.ts +2 -0
- package/dist/graph/react/index.d.ts.map +1 -0
- package/dist/graph/react/index.js +5 -0
- package/dist/graph/react/index.js.map +1 -0
- package/dist/graph/react/story-graph.d.ts +17 -0
- package/dist/graph/react/story-graph.d.ts.map +1 -0
- package/dist/graph/react/story-graph.js +26 -0
- package/dist/graph/react/story-graph.js.map +1 -0
- package/dist/graph/tones.d.ts +36 -0
- package/dist/graph/tones.d.ts.map +1 -0
- package/dist/graph/tones.js +38 -0
- package/dist/graph/tones.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/schema/constants.d.ts +3 -0
- package/dist/schema/constants.d.ts.map +1 -0
- package/dist/schema/constants.js +3 -0
- package/dist/schema/constants.js.map +1 -0
- package/dist/schema/extensions.d.ts +14 -0
- package/dist/schema/extensions.d.ts.map +1 -0
- package/dist/schema/extensions.js +61 -0
- package/dist/schema/extensions.js.map +1 -0
- package/dist/schema/generated-terms.d.ts +7 -0
- package/dist/schema/generated-terms.d.ts.map +1 -0
- package/dist/schema/generated-terms.js +518 -0
- package/dist/schema/generated-terms.js.map +1 -0
- package/dist/schema/generated.d.ts +64 -0
- package/dist/schema/generated.d.ts.map +1 -0
- package/dist/schema/generated.js +497 -0
- package/dist/schema/generated.js.map +1 -0
- package/dist/schema/index.d.ts +48 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +92 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/normalize.d.ts +20 -0
- package/dist/schema/normalize.d.ts.map +1 -0
- package/dist/schema/normalize.js +88 -0
- package/dist/schema/normalize.js.map +1 -0
- package/dist/schema/read.d.ts +31 -0
- package/dist/schema/read.d.ts.map +1 -0
- package/dist/schema/read.js +640 -0
- package/dist/schema/read.js.map +1 -0
- package/dist/schema/terms.d.ts +11 -0
- package/dist/schema/terms.d.ts.map +1 -0
- package/dist/schema/terms.js +80 -0
- package/dist/schema/terms.js.map +1 -0
- package/dist/schema/types.d.ts +140 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +2 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema/validate.d.ts +54 -0
- package/dist/schema/validate.d.ts.map +1 -0
- package/dist/schema/validate.js +490 -0
- package/dist/schema/validate.js.map +1 -0
- package/dist/store/index.d.ts +76 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +96 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +35 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +192 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/mutations.d.ts +45 -0
- package/dist/store/mutations.d.ts.map +1 -0
- package/dist/store/mutations.js +181 -0
- package/dist/store/mutations.js.map +1 -0
- package/dist/store/neon.d.ts +42 -0
- package/dist/store/neon.d.ts.map +1 -0
- package/dist/store/neon.js +167 -0
- package/dist/store/neon.js.map +1 -0
- package/dist/terminology/generated.d.ts +3 -0
- package/dist/terminology/generated.d.ts.map +1 -0
- package/dist/terminology/generated.js +2923 -0
- package/dist/terminology/generated.js.map +1 -0
- package/dist/terminology/index.d.ts +20 -0
- package/dist/terminology/index.d.ts.map +1 -0
- package/dist/terminology/index.js +71 -0
- package/dist/terminology/index.js.map +1 -0
- package/dist/terminology/types.d.ts +25 -0
- package/dist/terminology/types.d.ts.map +1 -0
- package/dist/terminology/types.js +2 -0
- package/dist/terminology/types.js.map +1 -0
- package/package.json +117 -0
- package/rispecs/README.rise.md +5 -0
- package/rispecs/RISPEC.md +9 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
// One reader for the NCP files that actually exist. Three dialects are in
|
|
2
|
+
// circulation and none of them is a defect: they are what different tools
|
|
3
|
+
// wrote. This module recognizes each one and returns canonical NCP plus a note
|
|
4
|
+
// for every adaptation it had to make.
|
|
5
|
+
//
|
|
6
|
+
// canonical `{ schema_version, story { narratives[...] } }`, snake_case values
|
|
7
|
+
// subtxt-export narratives beside `story`, perspectives keyed by pov,
|
|
8
|
+
// `context` strings, Dramatica display labels, placeholder ids
|
|
9
|
+
// miadi-design NCP container names with a design vocabulary
|
|
10
|
+
// (`label`, `description`, `event`, `structural_function`)
|
|
11
|
+
import { NCP_SCHEMA_VERSION } from './constants.js';
|
|
12
|
+
import { normalizeNcpInput } from './normalize.js';
|
|
13
|
+
import { canonicalDynamic, canonicalNarrativeFunction, canonicalStorybeatAppreciation, canonicalStorypointAppreciation, canonicalVector, povForContext, } from './terms.js';
|
|
14
|
+
const PLACEHOLDER_ID = /^\[\[.*\]\]$/;
|
|
15
|
+
function obj(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: undefined;
|
|
19
|
+
}
|
|
20
|
+
function str(value) {
|
|
21
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A collection that is not an array is read as empty and reported, rather than
|
|
25
|
+
* throwing: real exports carry objects where the schema says array.
|
|
26
|
+
*/
|
|
27
|
+
function arr(value, path, notes) {
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value;
|
|
30
|
+
if (value === undefined || value === null)
|
|
31
|
+
return [];
|
|
32
|
+
notes.push({ path, action: `read a ${typeof value} as an empty collection` });
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
function firstNarrative(input) {
|
|
36
|
+
const fromStory = obj(input['story'])?.['narratives'];
|
|
37
|
+
const candidates = Array.isArray(fromStory) ? fromStory : Array.isArray(input['narratives']) ? input['narratives'] : [];
|
|
38
|
+
return obj(candidates[0]);
|
|
39
|
+
}
|
|
40
|
+
/** Which dialect an input is written in. */
|
|
41
|
+
export function detectDialect(input) {
|
|
42
|
+
const root = obj(input);
|
|
43
|
+
if (!root)
|
|
44
|
+
return 'unknown';
|
|
45
|
+
const narrative = firstNarrative(root);
|
|
46
|
+
if (!narrative)
|
|
47
|
+
return 'unknown';
|
|
48
|
+
const subtext = obj(narrative['subtext']);
|
|
49
|
+
if (!subtext)
|
|
50
|
+
return 'unknown';
|
|
51
|
+
// Perspectives keyed by point of view, or `context` strings on elements.
|
|
52
|
+
const perspectives = subtext['perspectives'];
|
|
53
|
+
if (obj(perspectives) !== undefined)
|
|
54
|
+
return 'subtxt-export';
|
|
55
|
+
const firstBeat = obj(subtext['storybeats']?.[0]);
|
|
56
|
+
if (firstBeat) {
|
|
57
|
+
if (str(firstBeat['event']) !== undefined || str(firstBeat['structural_function']) !== undefined) {
|
|
58
|
+
return 'miadi-design';
|
|
59
|
+
}
|
|
60
|
+
if (str(firstBeat['context']) !== undefined && firstBeat['perspectives'] === undefined)
|
|
61
|
+
return 'subtxt-export';
|
|
62
|
+
if (str(firstBeat['id']) !== undefined && PLACEHOLDER_ID.test(firstBeat['id']))
|
|
63
|
+
return 'subtxt-export';
|
|
64
|
+
}
|
|
65
|
+
const firstPerspective = obj(perspectives?.[0]);
|
|
66
|
+
if (firstPerspective && str(firstPerspective['label']) !== undefined)
|
|
67
|
+
return 'miadi-design';
|
|
68
|
+
const firstPlayer = obj(subtext['players']?.[0]);
|
|
69
|
+
if (firstPlayer && str(firstPlayer['structural_role']) !== undefined)
|
|
70
|
+
return 'miadi-design';
|
|
71
|
+
return 'canonical';
|
|
72
|
+
}
|
|
73
|
+
/** Read any recognized dialect and return canonical NCP. */
|
|
74
|
+
export function readNcp(input, options) {
|
|
75
|
+
const dialect = options?.assume ?? detectDialect(input);
|
|
76
|
+
switch (dialect) {
|
|
77
|
+
case 'subtxt-export':
|
|
78
|
+
return readSubtxtExport(input, options);
|
|
79
|
+
case 'miadi-design':
|
|
80
|
+
return readMiadiDesign(input, options);
|
|
81
|
+
case 'canonical':
|
|
82
|
+
case 'unknown':
|
|
83
|
+
default: {
|
|
84
|
+
const normalized = normalizeNcpInput(input);
|
|
85
|
+
const canonicalized = canonicalizeTerms(normalized.document);
|
|
86
|
+
return {
|
|
87
|
+
document: applyOverrides(canonicalized.document, options),
|
|
88
|
+
dialect,
|
|
89
|
+
notes: [...normalized.notes, ...canonicalized.notes],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function applyOverrides(document, options) {
|
|
95
|
+
if (!options?.storyOverrides)
|
|
96
|
+
return document;
|
|
97
|
+
const overrides = Object.fromEntries(Object.entries(options.storyOverrides).filter(([, value]) => value !== undefined));
|
|
98
|
+
return { ...document, story: { ...document.story, ...overrides } };
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// subtxt-export
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
function readSubtxtExport(input, options) {
|
|
104
|
+
const notes = [];
|
|
105
|
+
const root = obj(input) ?? {};
|
|
106
|
+
const storyRaw = obj(root['story']) ?? {};
|
|
107
|
+
const narrativesRaw = Array.isArray(root['narratives'])
|
|
108
|
+
? root['narratives']
|
|
109
|
+
: arr(storyRaw['narratives'], '$.story.narratives', notes);
|
|
110
|
+
if (Array.isArray(root['narratives'])) {
|
|
111
|
+
notes.push({ path: '$.narratives', action: 'moved top-level `narratives` under `story`' });
|
|
112
|
+
}
|
|
113
|
+
const narratives = narrativesRaw.map((narrativeRaw, index) => readSubtxtNarrative(obj(narrativeRaw) ?? {}, `$.story.narratives[${index}]`, notes));
|
|
114
|
+
const story = {
|
|
115
|
+
id: str(storyRaw['id']) ?? options?.fallbackId ?? 'story',
|
|
116
|
+
title: str(storyRaw['title']) ?? 'Untitled story',
|
|
117
|
+
genre: str(storyRaw['genre']),
|
|
118
|
+
logline: str(storyRaw['logline']),
|
|
119
|
+
created_at: str(storyRaw['created_at']),
|
|
120
|
+
narratives,
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
document: applyOverrides({ schema_version: NCP_SCHEMA_VERSION, story }, options),
|
|
124
|
+
dialect: 'subtxt-export',
|
|
125
|
+
notes,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function readSubtxtNarrative(raw, path, notes) {
|
|
129
|
+
const subtextRaw = obj(raw['subtext']) ?? {};
|
|
130
|
+
// Perspectives arrive either as an array or as an object keyed by pov.
|
|
131
|
+
const perspectives = [];
|
|
132
|
+
const perspectivesRaw = subtextRaw['perspectives'];
|
|
133
|
+
const byContext = new Map();
|
|
134
|
+
const recordPerspective = (candidate, povHint) => {
|
|
135
|
+
const id = str(candidate['id']);
|
|
136
|
+
if (!id)
|
|
137
|
+
return;
|
|
138
|
+
const perspective = {
|
|
139
|
+
id,
|
|
140
|
+
author_structural_pov: (str(candidate['author_structural_pov']) ?? str(candidate['pov']) ?? povHint),
|
|
141
|
+
summary: str(candidate['summary']),
|
|
142
|
+
storytelling: str(candidate['storytelling']),
|
|
143
|
+
throughline: str(candidate['context']) ?? str(candidate['throughline']),
|
|
144
|
+
};
|
|
145
|
+
perspectives.push(perspective);
|
|
146
|
+
const context = str(candidate['context']);
|
|
147
|
+
if (context)
|
|
148
|
+
byContext.set(context.toLowerCase(), id);
|
|
149
|
+
};
|
|
150
|
+
if (Array.isArray(perspectivesRaw)) {
|
|
151
|
+
for (const candidate of perspectivesRaw) {
|
|
152
|
+
const holder = obj(candidate);
|
|
153
|
+
if (holder)
|
|
154
|
+
recordPerspective(holder);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const keyed = obj(perspectivesRaw);
|
|
159
|
+
if (keyed) {
|
|
160
|
+
notes.push({ path: `${path}.subtext.perspectives`, action: 'flattened perspectives keyed by point of view' });
|
|
161
|
+
for (const [pov, value] of Object.entries(keyed)) {
|
|
162
|
+
const list = Array.isArray(value) ? value : [value];
|
|
163
|
+
for (const candidate of list) {
|
|
164
|
+
const holder = obj(candidate);
|
|
165
|
+
if (holder)
|
|
166
|
+
recordPerspective(holder, pov);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const declaredIds = new Set(perspectives.map((perspective) => perspective.id));
|
|
172
|
+
const byPov = new Map();
|
|
173
|
+
for (const perspective of perspectives) {
|
|
174
|
+
const pov = perspective.author_structural_pov;
|
|
175
|
+
if (pov && !byPov.has(pov))
|
|
176
|
+
byPov.set(pov, perspective.id);
|
|
177
|
+
}
|
|
178
|
+
// These exports carry a fresh random `perspective_id` on every element, and
|
|
179
|
+
// none of them resolves. The real link is the `context` label, which names a
|
|
180
|
+
// throughline and therefore a point of view.
|
|
181
|
+
const resolveContext = (context) => {
|
|
182
|
+
if (!context)
|
|
183
|
+
return undefined;
|
|
184
|
+
const direct = byContext.get(context.toLowerCase());
|
|
185
|
+
if (direct)
|
|
186
|
+
return direct;
|
|
187
|
+
const pov = povForContext(context);
|
|
188
|
+
return pov ? byPov.get(pov) : undefined;
|
|
189
|
+
};
|
|
190
|
+
const refsFor = (holder, at) => {
|
|
191
|
+
const existing = holder['perspectives'];
|
|
192
|
+
const declaredRefs = [];
|
|
193
|
+
if (Array.isArray(existing)) {
|
|
194
|
+
for (const entry of existing) {
|
|
195
|
+
const id = str(obj(entry)?.['perspective_id']);
|
|
196
|
+
if (id)
|
|
197
|
+
declaredRefs.push(id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const single = obj(existing);
|
|
202
|
+
const id = single ? str(single['perspective_id']) : undefined;
|
|
203
|
+
if (id)
|
|
204
|
+
declaredRefs.push(id);
|
|
205
|
+
}
|
|
206
|
+
const resolvable = declaredRefs.filter((id) => declaredIds.has(id));
|
|
207
|
+
if (resolvable.length > 0) {
|
|
208
|
+
if (resolvable.length !== declaredRefs.length) {
|
|
209
|
+
notes.push({ path: `${at}.perspectives`, action: 'dropped references to undeclared perspectives' });
|
|
210
|
+
}
|
|
211
|
+
return resolvable.map((id) => ({ perspective_id: id }));
|
|
212
|
+
}
|
|
213
|
+
const context = str(holder['context']);
|
|
214
|
+
const resolved = resolveContext(context);
|
|
215
|
+
if (resolved) {
|
|
216
|
+
if (declaredRefs.length > 0) {
|
|
217
|
+
notes.push({
|
|
218
|
+
path: `${at}.perspectives`,
|
|
219
|
+
action: `reference \`${declaredRefs[0]}\` matches no declared perspective; used \`context\` \`${context}\` instead`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
notes.push({ path: `${at}.perspectives`, action: `resolved \`context\` to perspective \`${resolved}\`` });
|
|
224
|
+
}
|
|
225
|
+
return [{ perspective_id: resolved }];
|
|
226
|
+
}
|
|
227
|
+
if (declaredRefs.length > 0 || context) {
|
|
228
|
+
notes.push({
|
|
229
|
+
path: `${at}.perspectives`,
|
|
230
|
+
action: context
|
|
231
|
+
? `left unreferenced: \`context\` \`${context}\` names no declared perspective`
|
|
232
|
+
: 'left unreferenced: the perspective id matches no declared perspective',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
};
|
|
237
|
+
const players = arr(subtextRaw['players'], `${path}.subtext.players`, notes).map((candidate, index) => {
|
|
238
|
+
const holder = obj(candidate) ?? {};
|
|
239
|
+
const at = `${path}.subtext.players[${index}]`;
|
|
240
|
+
return {
|
|
241
|
+
id: str(holder['id']) ?? `player-${index + 1}`,
|
|
242
|
+
name: str(holder['name']) ?? `Player ${index + 1}`,
|
|
243
|
+
role: str(holder['role']) ?? '',
|
|
244
|
+
visual: str(holder['visual']),
|
|
245
|
+
audio: str(holder['audio']),
|
|
246
|
+
summary: str(holder['summary']) ?? str(holder['bio']),
|
|
247
|
+
storytelling: str(holder['storytelling']),
|
|
248
|
+
perspectives: refsFor(holder, at),
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
const dynamics = arr(subtextRaw['dynamics'], `${path}.subtext.dynamics`, notes).map((candidate, index) => {
|
|
252
|
+
const holder = obj(candidate) ?? {};
|
|
253
|
+
const at = `${path}.subtext.dynamics[${index}]`;
|
|
254
|
+
const rawDynamic = str(holder['dynamic']) ?? '';
|
|
255
|
+
const rawVector = str(holder['vector']) ?? '';
|
|
256
|
+
const dynamic = canonicalDynamic(rawDynamic);
|
|
257
|
+
const vector = canonicalVector(rawVector);
|
|
258
|
+
if (rawDynamic && !dynamic) {
|
|
259
|
+
notes.push({ path: `${at}.dynamic`, action: `declared \`${rawDynamic}\` in \`custom_dynamic\`` });
|
|
260
|
+
}
|
|
261
|
+
if (rawVector && !vector) {
|
|
262
|
+
notes.push({ path: `${at}.vector`, action: `declared \`${rawVector}\` in \`custom_vector\`` });
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
id: str(holder['id']) ?? `dynamic-${index + 1}`,
|
|
266
|
+
dynamic,
|
|
267
|
+
vector,
|
|
268
|
+
...(dynamic || !rawDynamic ? {} : { custom_dynamic: rawDynamic }),
|
|
269
|
+
...(vector || !rawVector ? {} : { custom_vector: rawVector }),
|
|
270
|
+
summary: str(holder['summary']),
|
|
271
|
+
storytelling: str(holder['storytelling']),
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
const storypoints = arr(subtextRaw['storypoints'], `${path}.subtext.storypoints`, notes).map((candidate, index) => {
|
|
275
|
+
const holder = obj(candidate) ?? {};
|
|
276
|
+
const at = `${path}.subtext.storypoints[${index}]`;
|
|
277
|
+
const rawAppreciation = str(holder['appreciation']) ?? '';
|
|
278
|
+
const rawFunction = str(holder['narrative_function']) ?? '';
|
|
279
|
+
const appreciation = canonicalStorypointAppreciation(rawAppreciation);
|
|
280
|
+
const narrativeFunction = canonicalNarrativeFunction(rawFunction, 'storypoint');
|
|
281
|
+
if (rawAppreciation && !appreciation) {
|
|
282
|
+
notes.push({ path: `${at}.appreciation`, action: `declared \`${rawAppreciation}\` as a custom appreciation` });
|
|
283
|
+
}
|
|
284
|
+
if (rawFunction && !narrativeFunction) {
|
|
285
|
+
notes.push({ path: `${at}.narrative_function`, action: `declared \`${rawFunction}\` as a custom function` });
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
id: str(holder['id']) ?? `storypoint-${index + 1}`,
|
|
289
|
+
appreciation: appreciation ?? 'domain',
|
|
290
|
+
narrative_function: narrativeFunction ?? 'universe',
|
|
291
|
+
...(appreciation || !rawAppreciation ? {} : { custom_appreciation: rawAppreciation }),
|
|
292
|
+
...(narrativeFunction || !rawFunction ? {} : { custom_narrative_function: rawFunction }),
|
|
293
|
+
illustration: str(holder['illustration']),
|
|
294
|
+
summary: str(holder['summary']),
|
|
295
|
+
storytelling: str(holder['storytelling']),
|
|
296
|
+
perspectives: refsFor(holder, at),
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
// Placeholder ids and null sequences are common in these exports. A beat's
|
|
300
|
+
// sequence numbers a series — one throughline's signposts, or its
|
|
301
|
+
// progressions — so an unnumbered beat is numbered after the last number that
|
|
302
|
+
// series already uses, never over it.
|
|
303
|
+
const beatsRaw = arr(subtextRaw['storybeats'], `${path}.subtext.storybeats`, notes);
|
|
304
|
+
const seriesKeyOf = (holder) => {
|
|
305
|
+
const context = (str(holder['context']) ?? 'unscoped').toLowerCase();
|
|
306
|
+
const appreciation = canonicalStorybeatAppreciation(str(holder['appreciation']) ?? '') ?? 'event';
|
|
307
|
+
return `${context}|${appreciation}`;
|
|
308
|
+
};
|
|
309
|
+
const seriesHighWater = new Map();
|
|
310
|
+
for (const candidate of beatsRaw) {
|
|
311
|
+
const holder = obj(candidate);
|
|
312
|
+
if (!holder)
|
|
313
|
+
continue;
|
|
314
|
+
const sequence = holder['sequence'];
|
|
315
|
+
if (typeof sequence !== 'number')
|
|
316
|
+
continue;
|
|
317
|
+
const key = seriesKeyOf(holder);
|
|
318
|
+
seriesHighWater.set(key, Math.max(seriesHighWater.get(key) ?? 0, sequence));
|
|
319
|
+
}
|
|
320
|
+
const storybeats = beatsRaw.map((candidate, index) => {
|
|
321
|
+
const holder = obj(candidate) ?? {};
|
|
322
|
+
const at = `${path}.subtext.storybeats[${index}]`;
|
|
323
|
+
const seriesKey = seriesKeyOf(holder);
|
|
324
|
+
const rawId = str(holder['id']);
|
|
325
|
+
let id = rawId;
|
|
326
|
+
if (!id || PLACEHOLDER_ID.test(id)) {
|
|
327
|
+
id = `beat-${index + 1}`;
|
|
328
|
+
notes.push({ path: `${at}.id`, action: `replaced a placeholder id with \`${id}\`` });
|
|
329
|
+
}
|
|
330
|
+
let sequence = typeof holder['sequence'] === 'number' ? holder['sequence'] : undefined;
|
|
331
|
+
if (sequence === undefined) {
|
|
332
|
+
sequence = (seriesHighWater.get(seriesKey) ?? 0) + 1;
|
|
333
|
+
seriesHighWater.set(seriesKey, sequence);
|
|
334
|
+
notes.push({ path: `${at}.sequence`, action: `numbered ${sequence}, after the \`${seriesKey}\` series` });
|
|
335
|
+
}
|
|
336
|
+
const rawAppreciation = str(holder['appreciation']) ?? '';
|
|
337
|
+
const appreciation = canonicalStorybeatAppreciation(rawAppreciation);
|
|
338
|
+
if (rawAppreciation && !appreciation) {
|
|
339
|
+
notes.push({ path: `${at}.appreciation`, action: `read \`${rawAppreciation}\` as \`event\`` });
|
|
340
|
+
}
|
|
341
|
+
const rawFunction = str(holder['narrative_function']) ?? '';
|
|
342
|
+
const narrativeFunction = canonicalNarrativeFunction(rawFunction, 'storybeat');
|
|
343
|
+
if (rawFunction && !narrativeFunction) {
|
|
344
|
+
notes.push({ path: `${at}.narrative_function`, action: `declared \`${rawFunction}\` as a custom function` });
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
id,
|
|
348
|
+
appreciation: (appreciation ?? 'event'),
|
|
349
|
+
sequence,
|
|
350
|
+
narrative_function: narrativeFunction ?? 'past',
|
|
351
|
+
...(narrativeFunction || !rawFunction ? {} : { custom_narrative_function: rawFunction }),
|
|
352
|
+
illustration: str(holder['illustration']),
|
|
353
|
+
summary: str(holder['summary']),
|
|
354
|
+
storytelling: str(holder['storytelling']),
|
|
355
|
+
tones: obj(holder['tones']),
|
|
356
|
+
perspectives: refsFor(holder, at),
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
const storytellingRaw = obj(raw['storytelling']) ?? {};
|
|
360
|
+
return {
|
|
361
|
+
id: str(raw['id']) ?? 'narrative',
|
|
362
|
+
title: str(raw['title']) ?? 'Untitled narrative',
|
|
363
|
+
subtext: { perspectives, players, dynamics, storypoints, storybeats },
|
|
364
|
+
storytelling: {
|
|
365
|
+
overviews: arr(storytellingRaw['overviews'], `${path}.storytelling.overviews`, notes).map((candidate, index) => {
|
|
366
|
+
const holder = obj(candidate) ?? {};
|
|
367
|
+
return {
|
|
368
|
+
id: str(holder['id']) ?? `overview-${index + 1}`,
|
|
369
|
+
summary: str(holder['summary']) ?? str(holder['title']),
|
|
370
|
+
storytelling: str(holder['storytelling']) ?? str(holder['description']),
|
|
371
|
+
};
|
|
372
|
+
}),
|
|
373
|
+
moments: arr(storytellingRaw['moments'], `${path}.storytelling.moments`, notes).map((candidate, index) => readMoment(obj(candidate) ?? {}, index)),
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function readMoment(holder, index) {
|
|
378
|
+
const beatRefs = holder['storybeats'] ?? holder['beat_references'];
|
|
379
|
+
const storybeats = Array.isArray(beatRefs)
|
|
380
|
+
? beatRefs.map((entry, refIndex) => {
|
|
381
|
+
if (typeof entry === 'string')
|
|
382
|
+
return { sequence: refIndex + 1, storybeat_id: entry };
|
|
383
|
+
const refHolder = obj(entry) ?? {};
|
|
384
|
+
return {
|
|
385
|
+
sequence: typeof refHolder['sequence'] === 'number' ? refHolder['sequence'] : refIndex + 1,
|
|
386
|
+
storybeat_id: String(refHolder['storybeat_id'] ?? ''),
|
|
387
|
+
};
|
|
388
|
+
})
|
|
389
|
+
: undefined;
|
|
390
|
+
return {
|
|
391
|
+
id: str(holder['id']) ?? `moment-${index + 1}`,
|
|
392
|
+
summary: str(holder['summary']) ?? str(holder['title']),
|
|
393
|
+
synopsis: str(holder['synopsis']) ?? str(holder['scene_summary']),
|
|
394
|
+
setting: str(holder['setting']),
|
|
395
|
+
timing: str(holder['timing']),
|
|
396
|
+
imperatives: str(holder['imperatives']),
|
|
397
|
+
fabric: Array.isArray(holder['fabric']) ? holder['fabric'] : undefined,
|
|
398
|
+
audience_experiential_pov: str(holder['audience_experiential_pov']),
|
|
399
|
+
storybeats,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// miadi-design
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
/**
|
|
406
|
+
* The mapping the prototype's `adaptCatalystStory` established, kept as the
|
|
407
|
+
* contract: design fields land on the NCP field that carries the same load, and
|
|
408
|
+
* nothing is invented for a field the source does not have.
|
|
409
|
+
*/
|
|
410
|
+
function readMiadiDesign(input, options) {
|
|
411
|
+
const notes = [];
|
|
412
|
+
const root = obj(input) ?? {};
|
|
413
|
+
const storyRaw = obj(root['story']) ?? root;
|
|
414
|
+
const narrativesRaw = arr(storyRaw['narratives'], '$.story.narratives', notes);
|
|
415
|
+
const narratives = narrativesRaw.map((narrativeRaw, index) => {
|
|
416
|
+
const raw = obj(narrativeRaw) ?? {};
|
|
417
|
+
const path = `$.story.narratives[${index}]`;
|
|
418
|
+
const subtextRaw = obj(raw['subtext']) ?? {};
|
|
419
|
+
const perspectives = arr(subtextRaw['perspectives'], `${path}.subtext.perspectives`, notes).map((candidate, i) => {
|
|
420
|
+
const holder = obj(candidate) ?? {};
|
|
421
|
+
return {
|
|
422
|
+
id: str(holder['id']) ?? `perspective-${i + 1}`,
|
|
423
|
+
summary: str(holder['label']) ?? str(holder['summary']),
|
|
424
|
+
storytelling: str(holder['description']) ?? str(holder['storytelling']),
|
|
425
|
+
throughline: str(holder['thematic_question']),
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
if (perspectives.length > 0) {
|
|
429
|
+
notes.push({
|
|
430
|
+
path: `${path}.subtext.perspectives`,
|
|
431
|
+
action: 'read `label`/`description` as summary/storytelling; left `author_structural_pov` unset',
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const players = arr(subtextRaw['players'], `${path}.subtext.players`, notes).map((candidate, i) => {
|
|
435
|
+
const holder = obj(candidate) ?? {};
|
|
436
|
+
return {
|
|
437
|
+
id: str(holder['id']) ?? `player-${i + 1}`,
|
|
438
|
+
name: str(holder['name']) ?? `Player ${i + 1}`,
|
|
439
|
+
role: str(holder['structural_role']) ?? str(holder['role']) ?? '',
|
|
440
|
+
summary: str(holder['function']) ?? str(holder['summary']),
|
|
441
|
+
storytelling: str(holder['thematic_representation']) ?? str(holder['arc']) ?? str(holder['storytelling']),
|
|
442
|
+
motivations: str(holder['wound'])
|
|
443
|
+
? [{ narrative_function: 'wound', illustration: holder['wound'] }]
|
|
444
|
+
: undefined,
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
const dynamics = arr(subtextRaw['dynamics'], `${path}.subtext.dynamics`, notes).map((candidate, i) => {
|
|
448
|
+
const holder = obj(candidate) ?? {};
|
|
449
|
+
const label = str(holder['label']) ?? str(holder['dynamic']) ?? '';
|
|
450
|
+
const mapped = canonicalDynamic(label);
|
|
451
|
+
const rawVector = str(holder['vector']) ?? '';
|
|
452
|
+
const vector = canonicalVector(rawVector);
|
|
453
|
+
if (label && !mapped) {
|
|
454
|
+
notes.push({
|
|
455
|
+
path: `${path}.subtext.dynamics[${i}]`,
|
|
456
|
+
action: `\`${label}\` is not a Dramatica dynamic; declared it in \`custom_dynamic\``,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
id: str(holder['id']) ?? `dynamic-${i + 1}`,
|
|
461
|
+
dynamic: mapped,
|
|
462
|
+
vector,
|
|
463
|
+
...(mapped || !label ? {} : { custom_dynamic: label }),
|
|
464
|
+
...(vector || !rawVector ? {} : { custom_vector: rawVector }),
|
|
465
|
+
summary: str(holder['description']) ?? str(holder['summary']),
|
|
466
|
+
storytelling: str(holder['manifestation']) ?? str(holder['thematic_significance']),
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
const storypoints = arr(subtextRaw['storypoints'], `${path}.subtext.storypoints`, notes).map((candidate, i) => {
|
|
470
|
+
const holder = obj(candidate) ?? {};
|
|
471
|
+
const label = str(holder['label']) ?? '';
|
|
472
|
+
const mapped = canonicalStorypointAppreciation(label.split(':')[0] ?? '');
|
|
473
|
+
return {
|
|
474
|
+
id: str(holder['id']) ?? `storypoint-${i + 1}`,
|
|
475
|
+
appreciation: mapped ?? 'domain',
|
|
476
|
+
narrative_function: 'universe',
|
|
477
|
+
...(mapped || !label.split(':')[0]?.trim() ? {} : { custom_appreciation: label.split(':')[0].trim() }),
|
|
478
|
+
summary: label || str(holder['summary']),
|
|
479
|
+
storytelling: str(holder['description']) ?? str(holder['storytelling']),
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
if (storypoints.length > 0) {
|
|
483
|
+
notes.push({
|
|
484
|
+
path: `${path}.subtext.storypoints`,
|
|
485
|
+
action: 'declared design labels as custom appreciations; canonical narrative functions are unset',
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const storybeats = arr(subtextRaw['storybeats'], `${path}.subtext.storybeats`, notes).map((candidate, i) => {
|
|
489
|
+
const holder = obj(candidate) ?? {};
|
|
490
|
+
const event = str(holder['event']);
|
|
491
|
+
const structural = str(holder['structural_function']);
|
|
492
|
+
const emotional = str(holder['emotional_weight']);
|
|
493
|
+
const appreciation = canonicalStorybeatAppreciation(structural ?? '') ?? 'event';
|
|
494
|
+
const storytelling = emotional && event ? `${event}\n\n[Emotional weight: ${emotional}]` : (event ?? undefined);
|
|
495
|
+
return {
|
|
496
|
+
id: str(holder['id']) ?? `beat-${i + 1}`,
|
|
497
|
+
appreciation: appreciation,
|
|
498
|
+
sequence: typeof holder['sequence'] === 'number' ? holder['sequence'] : i + 1,
|
|
499
|
+
narrative_function: canonicalNarrativeFunction(structural ?? '', 'storybeat') ?? 'past',
|
|
500
|
+
...(structural && !canonicalNarrativeFunction(structural, 'storybeat')
|
|
501
|
+
? { custom_narrative_function: structural }
|
|
502
|
+
: {}),
|
|
503
|
+
illustration: event,
|
|
504
|
+
summary: structural ?? (event ? `${event.slice(0, 60)}…` : undefined),
|
|
505
|
+
storytelling,
|
|
506
|
+
tones: obj(holder['tones']),
|
|
507
|
+
};
|
|
508
|
+
});
|
|
509
|
+
if (storybeats.length > 0) {
|
|
510
|
+
notes.push({
|
|
511
|
+
path: `${path}.subtext.storybeats`,
|
|
512
|
+
action: 'read `event` as illustration, `structural_function` as summary, `emotional_weight` into storytelling',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
const storytellingRaw = obj(raw['storytelling']) ?? {};
|
|
516
|
+
return {
|
|
517
|
+
id: str(raw['id']) ?? 'narrative',
|
|
518
|
+
title: str(raw['title']) ?? 'Untitled narrative',
|
|
519
|
+
subtext: { perspectives, players, dynamics, storypoints, storybeats },
|
|
520
|
+
storytelling: {
|
|
521
|
+
overviews: arr(storytellingRaw['overviews'], `${path}.storytelling.overviews`, notes).map((candidate, i) => {
|
|
522
|
+
const holder = obj(candidate) ?? {};
|
|
523
|
+
return {
|
|
524
|
+
id: str(holder['id']) ?? `overview-${i + 1}`,
|
|
525
|
+
summary: str(holder['title']) ?? str(holder['summary']),
|
|
526
|
+
storytelling: str(holder['description']) ?? str(holder['storytelling']),
|
|
527
|
+
};
|
|
528
|
+
}),
|
|
529
|
+
moments: arr(storytellingRaw['moments'], `${path}.storytelling.moments`, notes).map((candidate, i) => readMoment(obj(candidate) ?? {}, i)),
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
});
|
|
533
|
+
const story = {
|
|
534
|
+
id: str(storyRaw['id']) ?? options?.fallbackId ?? 'story',
|
|
535
|
+
title: str(storyRaw['title']) ?? 'Untitled story',
|
|
536
|
+
genre: str(storyRaw['genre']),
|
|
537
|
+
logline: str(storyRaw['logline']),
|
|
538
|
+
created_at: str(storyRaw['created_at']) ?? str(storyRaw['date_written']),
|
|
539
|
+
narratives,
|
|
540
|
+
};
|
|
541
|
+
return {
|
|
542
|
+
document: applyOverrides({ schema_version: NCP_SCHEMA_VERSION, story }, options),
|
|
543
|
+
dialect: 'miadi-design',
|
|
544
|
+
notes,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// canonical dialect: display labels to identifiers
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
/**
|
|
551
|
+
* A canonical-layout document can still carry Dramatica display labels where
|
|
552
|
+
* the schema declares identifiers — `example-story.json` does. This maps them
|
|
553
|
+
* and declares whatever it cannot map, so a consumer never has to guess whether
|
|
554
|
+
* `"Success"` and `"success"` are the same value.
|
|
555
|
+
*/
|
|
556
|
+
export function canonicalizeTerms(document) {
|
|
557
|
+
const notes = [];
|
|
558
|
+
const narratives = document.story.narratives.map((narrative, narrativeIndex) => {
|
|
559
|
+
const path = `$.story.narratives[${narrativeIndex}]`;
|
|
560
|
+
const dynamics = narrative.subtext.dynamics.map((dynamic, index) => {
|
|
561
|
+
const at = `${path}.subtext.dynamics[${index}]`;
|
|
562
|
+
const rawDynamic = typeof dynamic.dynamic === 'string' ? dynamic.dynamic : '';
|
|
563
|
+
const rawVector = typeof dynamic.vector === 'string' ? dynamic.vector : '';
|
|
564
|
+
const mappedDynamic = rawDynamic ? canonicalDynamic(rawDynamic) : undefined;
|
|
565
|
+
const mappedVector = rawVector ? canonicalVector(rawVector) : undefined;
|
|
566
|
+
if (rawDynamic && mappedDynamic && mappedDynamic !== rawDynamic) {
|
|
567
|
+
notes.push({ path: `${at}.dynamic`, action: `read \`${rawDynamic}\` as \`${mappedDynamic}\`` });
|
|
568
|
+
}
|
|
569
|
+
if (rawVector && mappedVector && mappedVector !== rawVector) {
|
|
570
|
+
notes.push({ path: `${at}.vector`, action: `read \`${rawVector}\` as \`${mappedVector}\`` });
|
|
571
|
+
}
|
|
572
|
+
if (rawDynamic && !mappedDynamic) {
|
|
573
|
+
notes.push({ path: `${at}.dynamic`, action: `declared \`${rawDynamic}\` in \`custom_dynamic\`` });
|
|
574
|
+
}
|
|
575
|
+
if (rawVector && !mappedVector) {
|
|
576
|
+
notes.push({ path: `${at}.vector`, action: `declared \`${rawVector}\` in \`custom_vector\`` });
|
|
577
|
+
}
|
|
578
|
+
const customDynamic = mappedDynamic ? dynamic.custom_dynamic : rawDynamic || dynamic.custom_dynamic;
|
|
579
|
+
const customVector = mappedVector ? dynamic.custom_vector : rawVector || dynamic.custom_vector;
|
|
580
|
+
return {
|
|
581
|
+
...dynamic,
|
|
582
|
+
...(mappedDynamic === undefined ? {} : { dynamic: mappedDynamic }),
|
|
583
|
+
...(mappedVector === undefined ? {} : { vector: mappedVector }),
|
|
584
|
+
...(customDynamic === undefined ? {} : { custom_dynamic: customDynamic }),
|
|
585
|
+
...(customVector === undefined ? {} : { custom_vector: customVector }),
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
const storypoints = narrative.subtext.storypoints.map((storypoint, index) => {
|
|
589
|
+
const at = `${path}.subtext.storypoints[${index}]`;
|
|
590
|
+
const rawAppreciation = typeof storypoint.appreciation === 'string' ? storypoint.appreciation : '';
|
|
591
|
+
const rawFunction = typeof storypoint.narrative_function === 'string' ? storypoint.narrative_function : '';
|
|
592
|
+
const appreciation = rawAppreciation ? canonicalStorypointAppreciation(rawAppreciation) : undefined;
|
|
593
|
+
const narrativeFunction = rawFunction ? canonicalNarrativeFunction(rawFunction, 'storypoint') : undefined;
|
|
594
|
+
if (rawAppreciation && appreciation && appreciation !== rawAppreciation) {
|
|
595
|
+
notes.push({ path: `${at}.appreciation`, action: `read \`${rawAppreciation}\` as \`${appreciation}\`` });
|
|
596
|
+
}
|
|
597
|
+
if (rawFunction && narrativeFunction && narrativeFunction !== rawFunction) {
|
|
598
|
+
notes.push({ path: `${at}.narrative_function`, action: `read \`${rawFunction}\` as \`${narrativeFunction}\`` });
|
|
599
|
+
}
|
|
600
|
+
const customAppreciation = appreciation
|
|
601
|
+
? storypoint.custom_appreciation
|
|
602
|
+
: rawAppreciation || storypoint.custom_appreciation;
|
|
603
|
+
const customFunction = narrativeFunction
|
|
604
|
+
? storypoint.custom_narrative_function
|
|
605
|
+
: rawFunction || storypoint.custom_narrative_function;
|
|
606
|
+
return {
|
|
607
|
+
...storypoint,
|
|
608
|
+
appreciation: appreciation ?? storypoint.appreciation,
|
|
609
|
+
narrative_function: narrativeFunction ?? storypoint.narrative_function,
|
|
610
|
+
...(customAppreciation === undefined ? {} : { custom_appreciation: customAppreciation }),
|
|
611
|
+
...(customFunction === undefined ? {} : { custom_narrative_function: customFunction }),
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
const storybeats = narrative.subtext.storybeats.map((beat, index) => {
|
|
615
|
+
const at = `${path}.subtext.storybeats[${index}]`;
|
|
616
|
+
const rawAppreciation = typeof beat.appreciation === 'string' ? beat.appreciation : '';
|
|
617
|
+
const rawFunction = typeof beat.narrative_function === 'string' ? beat.narrative_function : '';
|
|
618
|
+
const appreciation = rawAppreciation ? canonicalStorybeatAppreciation(rawAppreciation) : undefined;
|
|
619
|
+
const narrativeFunction = rawFunction ? canonicalNarrativeFunction(rawFunction, 'storybeat') : undefined;
|
|
620
|
+
if (rawAppreciation && appreciation && appreciation !== rawAppreciation) {
|
|
621
|
+
notes.push({ path: `${at}.appreciation`, action: `read \`${rawAppreciation}\` as \`${appreciation}\`` });
|
|
622
|
+
}
|
|
623
|
+
if (rawFunction && narrativeFunction && narrativeFunction !== rawFunction) {
|
|
624
|
+
notes.push({ path: `${at}.narrative_function`, action: `read \`${rawFunction}\` as \`${narrativeFunction}\`` });
|
|
625
|
+
}
|
|
626
|
+
const customFunction = narrativeFunction
|
|
627
|
+
? beat.custom_narrative_function
|
|
628
|
+
: rawFunction || beat.custom_narrative_function;
|
|
629
|
+
return {
|
|
630
|
+
...beat,
|
|
631
|
+
appreciation: (appreciation ?? beat.appreciation),
|
|
632
|
+
narrative_function: narrativeFunction ?? beat.narrative_function,
|
|
633
|
+
...(customFunction === undefined ? {} : { custom_narrative_function: customFunction }),
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
return { ...narrative, subtext: { ...narrative.subtext, dynamics, storypoints, storybeats } };
|
|
637
|
+
});
|
|
638
|
+
return { document: { ...document, story: { ...document.story, narratives } }, notes };
|
|
639
|
+
}
|
|
640
|
+
//# sourceMappingURL=read.js.map
|