nx_storydb.nx source
↩ module page · 252 lines · 8624 B
1// nx_storydb.nx -- top-level container for ingested narrative.
2//
3// A StoryDb holds everything produced by an ingestion session:
4// - beats arena (every StoryBeat across every medium)
5// - scenes arena (groups of consecutive related beats)
6// - chapters arena (groups of scenes that share arc context)
7// - media_pool (raw bytes of every ingested item)
8// - summary arena (string bytes for synopsis / extracted text)
9//
10// One StoryDb per ingestion session. Multiple sessions for the
11// same user merge into the persistent corpus through nx_corpus_
12// (separate module, not built yet).
13//
14// All arenas are fixed-capacity append-only; downstream consumers
15// hold cheap i64 indices, never pointers.
16//
17// See: docs/NISHI_MEDIA_INGESTION_ROADMAP.md
18//
19// nx_safety_envelope:
20// intended_use: "Top-level container for one ingestion
21// session. Holds beats / scenes / chapters /
22// media-pool / summary-strings."
23// sil_target: SIL2
24// asil_target: QM
25// dal_target: DAL C
26// iec_62304_class: NONE
27// evidence: [no_floating_point,
28// fixed_capacity_append_only_arenas,
29// cheap_i64_indices_no_pointers_to_caller,
30// media_pool_reference_indirected]
31// hazard_register: [bug-tape-arena-overflow-silent,
32// bug-tape-scene-spans-disjoint-from-beats]
33// residual_risk: "Default caps may overflow on book-length
34// ingestions; caller can request larger
35// StoryDb via nx_storydb_alloc_sized."
36// verdict: NOT_YET_EVALUATED
37
38import "nx_syscalls.nx"
39import "nx_storybeat.nx"
40import "nx_media_pool.nx"
41const NX_MAGIC_1024: i64 = 1024
42
43// ===== StoryScene =================================================
44//
45// Sequence of consecutive beats that share location / time /
46// dramatic unit. beat_first/beat_last are inclusive indices into
47// StoryDb.beats.
48
49struct StoryScene {
50 beat_first: i64,
51 beat_last: i64,
52 location_id: i64,
53 arc_stage: i64, // NX_ARCSTAGE_*
54 domain_code: i64, // dominant NX_DOMAIN_*
55 title_off: i64, // offset into summary arena
56 title_len: i64,
57 flags: i64, // reserved
58}
59
60const NX_STORYSCENE_BYTES: i64 = 64
61
62// ===== StoryChapter ===============================================
63//
64// Sequence of scenes that share narrative arc. Top-level structural
65// unit (a literotica "chapter", a video "act", an album "part").
66
67struct StoryChapter {
68 scene_first: i64,
69 scene_last: i64,
70 arc_id: i64, // 0 if no fitted arc
71 title_off: i64,
72 title_len: i64,
73 flags: i64,
74}
75
76const NX_STORYCHAPTER_BYTES: i64 = 48
77
78// ===== StoryDb container ==========================================
79
80const NX_STORYDB_BEATS_CAP: i64 = 65536 // 8 MiB worth of beats
81const NX_STORYDB_SCENES_CAP: i64 = 4096
82const NX_STORYDB_CHAPTERS_CAP: i64 = 256
83const NX_STORYDB_SUMMARY_CAP: i64 = 1048576 // 1 MiB summary text
84
85struct StoryDb {
86 beats: *StoryBeat,
87 n_beats: i64,
88 beats_cap: i64,
89
90 scenes: *StoryScene,
91 n_scenes: i64,
92 scenes_cap: i64,
93
94 chapters: *StoryChapter,
95 n_chapters: i64,
96 chapters_cap: i64,
97
98 summary_buf: *u8,
99 summary_len: i64,
100 summary_cap: i64,
101
102 media_pool: *MediaPool, // shared; owned by StoryDb
103}
104
105const NX_STORYDB_BYTES: i64 = 120
106
107// ===== allocation =================================================
108
109func nx_storydb_alloc_sized(beats_cap: i64, scenes_cap: i64,
110 chapters_cap: i64, summary_cap: i64,
111 media_cap: i64) -> *StoryDb {
112 var bc: i64 = beats_cap
113 var sc: i64 = scenes_cap
114 var cc: i64 = chapters_cap
115 var uc: i64 = summary_cap
116 var mc: i64 = media_cap
117 if bc <= 0 { bc = NX_STORYDB_BEATS_CAP }
118 if sc <= 0 { sc = NX_STORYDB_SCENES_CAP }
119 if cc <= 0 { cc = NX_STORYDB_CHAPTERS_CAP }
120 if uc <= 0 { uc = NX_STORYDB_SUMMARY_CAP }
121 if mc <= 0 { mc = NX_MAGIC_1024 }
122
123 let raw: *u8 = sys_mmap(NX_STORYDB_BYTES)
124 let db: *StoryDb = raw as *StoryDb
125 db.beats = (sys_mmap(bc * NX_STORYBEAT_BYTES)) as *StoryBeat
126 db.n_beats = 0
127 db.beats_cap = bc
128 db.scenes = (sys_mmap(sc * NX_STORYSCENE_BYTES)) as *StoryScene
129 db.n_scenes = 0
130 db.scenes_cap = sc
131 db.chapters = (sys_mmap(cc * NX_STORYCHAPTER_BYTES)) as *StoryChapter
132 db.n_chapters = 0
133 db.chapters_cap = cc
134 db.summary_buf = sys_mmap(uc)
135 db.summary_len = 0
136 db.summary_cap = uc
137 db.media_pool = nx_media_pool_alloc(mc)
138 return db
139}
140
141func nx_storydb_alloc() -> *StoryDb {
142 return nx_storydb_alloc_sized(0, 0, 0, 0, 0)
143}
144
145// ===== accessors ==================================================
146
147func nx_storydb_beat_at(db: *StoryDb, i: i64) -> *StoryBeat {
148 return (((db.beats as i64) + i * NX_STORYBEAT_BYTES) as *StoryBeat)
149}
150
151func nx_storydb_scene_at(db: *StoryDb, i: i64) -> *StoryScene {
152 return (((db.scenes as i64) + i * NX_STORYSCENE_BYTES) as *StoryScene)
153}
154
155func nx_storydb_chapter_at(db: *StoryDb, i: i64) -> *StoryChapter {
156 return (((db.chapters as i64) + i * NX_STORYCHAPTER_BYTES) as *StoryChapter)
157}
158
159// ===== append helpers =============================================
160
161// Returns the new beat id (= index into beats), or -1 on overflow.
162func nx_storydb_push_beat(db: *StoryDb, beat_kind: i64, media_type: i64,
163 media_id: i64, span_start: i64,
164 span_end: i64) -> i64 {
165 if db.n_beats >= db.beats_cap { return -1 }
166 let id: i64 = db.n_beats
167 let b: *StoryBeat = nx_storydb_beat_at(db, id)
168 nx_beat_init(b, beat_kind, media_type, media_id, span_start, span_end)
169 db.n_beats = db.n_beats + 1
170 return id
171}
172
173// Open a new scene starting at beat_first. Returns scene_id or -1.
174// scene.beat_last is left set to beat_first until nx_storydb_close_scene.
175func nx_storydb_open_scene(db: *StoryDb, beat_first: i64,
176 location_id: i64, arc_stage: i64,
177 domain_code: i64) -> i64 {
178 if db.n_scenes >= db.scenes_cap { return -1 }
179 let id: i64 = db.n_scenes
180 let s: *StoryScene = nx_storydb_scene_at(db, id)
181 s.beat_first = beat_first
182 s.beat_last = beat_first
183 s.location_id = location_id
184 s.arc_stage = arc_stage
185 s.domain_code = domain_code
186 s.title_off = 0
187 s.title_len = 0
188 s.flags = 0
189 db.n_scenes = db.n_scenes + 1
190 return id
191}
192
193func nx_storydb_close_scene(db: *StoryDb, scene_id: i64, beat_last: i64) {
194 if scene_id < 0 { return }
195 if scene_id >= db.n_scenes { return }
196 let s: *StoryScene = nx_storydb_scene_at(db, scene_id)
197 s.beat_last = beat_last
198}
199
200func nx_storydb_open_chapter(db: *StoryDb, scene_first: i64,
201 arc_id: i64) -> i64 {
202 if db.n_chapters >= db.chapters_cap { return -1 }
203 let id: i64 = db.n_chapters
204 let c: *StoryChapter = nx_storydb_chapter_at(db, id)
205 c.scene_first = scene_first
206 c.scene_last = scene_first
207 c.arc_id = arc_id
208 c.title_off = 0
209 c.title_len = 0
210 c.flags = 0
211 db.n_chapters = db.n_chapters + 1
212 return id
213}
214
215func nx_storydb_close_chapter(db: *StoryDb, chapter_id: i64,
216 scene_last: i64) {
217 if chapter_id < 0 { return }
218 if chapter_id >= db.n_chapters { return }
219 let c: *StoryChapter = nx_storydb_chapter_at(db, chapter_id)
220 c.scene_last = scene_last
221}
222
223// ===== byte-level helpers (must precede their callers) ============
224
225func nx_storydb_load_u8(p: *u8, i: i64) -> i64 {
226 let q: *u8 = ((p as i64) + i) as *u8
227 return *q
228}
229
230func nx_storydb_store_u8(p: *u8, i: i64, v: i64) {
231 let q: *u8 = ((p as i64) + i) as *u8
232 *q = v as u8
233}
234
235// ===== summary arena helpers ======================================
236//
237// Append a UTF-8 string into the summary arena and return its
238// offset. Returns -1 on overflow. Caller stores (offset, length)
239// in the beat / scene / chapter title fields.
240
241func nx_storydb_intern_summary(db: *StoryDb, s: *u8, n: i64) -> i64 {
242 if db.summary_len + n > db.summary_cap { return -1 }
243 let off: i64 = db.summary_len
244 var i: i64 = 0
245 while i < n {
246 let src_byte: i64 = nx_storydb_load_u8(s, i)
247 nx_storydb_store_u8(db.summary_buf, off + i, src_byte)
248 i = i + 1
249 }
250 db.summary_len = db.summary_len + n
251 return off
252}