nx_arc.nx source
↩ module page · 324 lines · 12812 B
1// nx_arc.nx -- canonical arc + arc-stage substrate.
2//
3// Arcs are the "events" of the day-as-a-movie metaphor (per cardinal
4// feedback-arc-and-moment-are-king-cinematographer-pattern). Each arc
5// is a sequence of stages; each stage has pools of allowed locations,
6// outfits, poses, expressions, and camera angles from which moments
7// are drawn.
8//
9// nx_moment_resolve.nx (next brick) consumes Arc + ArcStage and emits
10// a fully-populated *DirectorsNote (from nx_directors_note.nx).
11//
12// === Flat-array layout pattern (per nx_canon_proportions) ============
13//
14// To keep struct shapes fixed-size and avoid variable-length field
15// pointers (which trip the substrate's <=8-arg codegen lesson), this
16// primitive uses TWO flat i64 buffers attached to Arc:
17//
18// arc.stages : flat array, NX_ARC_STAGE_FIELDS i64 per stage
19// arc.pools : flat array of pool ID values; stages reference
20// their pools via (offset, len) indices into this buffer
21//
22// This is the same shape nx_canon_proportions uses for its 30-entry
23// canon table (5 i64 per entry, accessed via nx_canon_get).
24//
25// === Composition =====================================================
26//
27// nx_directors_note.nx -- ArcSource sealed-enum is re-exported here
28// so the same value can flow into a
29// DirectorsNote.source_tag
30// nx_tier.nx -- nx_int alias
31// nx_string_ops.nx -- (when caller wants to attach string names)
32//
33// genealogy_id: propp_morphology + campbell_monomyth + lindenmayer_systems +
34// syd_field_three_act_structure
35// lineage_id: nx_arc_v1
36
37// nx_safety_envelope:
38// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
39// sil_target: SIL1
40// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
41// verdict: NOT_YET_EVALUATED
42
43import "nx_syscalls.nx"
44import "nx_tier.nx"
45
46// ===== Sealed-enum: ArcSource =====================================
47//
48// How the arc was created. Mirrors the Python ArcSource semantics
49// but typed as i64 sealed constants per the NishiLang convention.
50
51const NX_AS_AUTHORED: nx_int = 0 // hand-curated arc in DB
52const NX_AS_DYNAMIC: nx_int = 1 // LLM-generated from chat
53const NX_AS_GENERATED: nx_int = 2 // story-engine pre-generated
54const NX_AS_COMPANION: nx_int = 3 // companion authored her own arc
55const NX_AS_N_SOURCES: nx_int = 4
56
57func nx_as_source_is_valid(s: nx_int) -> nx_int {
58 if s < 0 { return 0 }
59 if s >= NX_AS_N_SOURCES { return 0 }
60 return 1
61}
62
63// ===== Sealed-enum: Awareness =====================================
64//
65// How aware the character is of being observed. Drives the
66// downstream Perspective + framing decisions.
67
68const NX_AW_UNAWARE: nx_int = 0 // doesn't know she's seen
69const NX_AW_PEEKING: nx_int = 1 // hint of awareness
70const NX_AW_AWARE: nx_int = 2 // knows but acting natural
71const NX_AW_EYE_CONTACT: nx_int = 3 // looking at camera/viewer
72const NX_AW_DIRECT_ENGAGEMENT: nx_int = 4 // actively addressing
73const NX_AW_N_LEVELS: nx_int = 5
74
75func nx_aw_level_is_valid(a: nx_int) -> nx_int {
76 if a < 0 { return 0 }
77 if a >= NX_AW_N_LEVELS { return 0 }
78 return 1
79}
80
81// ===== Sealed-enum: EmotionalTone =================================
82//
83// Stage-level emotional tone. Per cardinal feedback-graduated-
84// intervention: this is a TYPED axis, not a free-text string.
85// Composes with nx_directors_note.AffectAxis (per-moment) — the
86// stage-tone bounds the per-moment-affect.
87
88const NX_ET_NEUTRAL: nx_int = 0
89const NX_ET_TENDER: nx_int = 1
90const NX_ET_PLAYFUL: nx_int = 2
91const NX_ET_INTENSE: nx_int = 3
92const NX_ET_LONGING: nx_int = 4
93const NX_ET_TRIUMPHANT: nx_int = 5
94const NX_ET_REVERENT: nx_int = 6
95const NX_ET_DOMESTIC: nx_int = 7 // mundane chores, cooking, etc.
96const NX_ET_ANTICIPATORY: nx_int = 8
97const NX_ET_AFTERGLOW: nx_int = 9
98const NX_ET_N_TONES: nx_int = 10
99
100func nx_et_tone_is_valid(t: nx_int) -> nx_int {
101 if t < 0 { return 0 }
102 if t >= NX_ET_N_TONES { return 0 }
103 return 1
104}
105
106// ===== Stage field layout (flat array offsets) =====================
107//
108// Each ArcStage occupies NX_ARC_STAGE_FIELDS i64 slots in the arc's
109// flat stages buffer. Field offsets:
110
111const NX_AST_F_STAGE_NUMBER: nx_int = 0
112const NX_AST_F_AWARENESS: nx_int = 1
113const NX_AST_F_EMOTIONAL_TONE: nx_int = 2
114const NX_AST_F_MIN_AROUSAL_Q10: nx_int = 3
115const NX_AST_F_MAX_AROUSAL_Q10: nx_int = 4
116const NX_AST_F_IMAGES_BEFORE_ADVANCE: nx_int = 5
117const NX_AST_F_LOCATION_POOL_OFF: nx_int = 6 // offset into arc.pools
118const NX_AST_F_LOCATION_POOL_LEN: nx_int = 7
119const NX_AST_F_OUTFIT_POOL_OFF: nx_int = 8
120const NX_AST_F_OUTFIT_POOL_LEN: nx_int = 9
121const NX_AST_F_POSE_POOL_OFF: nx_int = 10
122const NX_AST_F_POSE_POOL_LEN: nx_int = 11
123const NX_AST_F_EXPRESSION_POOL_OFF: nx_int = 12
124const NX_AST_F_EXPRESSION_POOL_LEN: nx_int = 13
125const NX_AST_F_CAMERA_POOL_OFF: nx_int = 14
126const NX_AST_F_CAMERA_POOL_LEN: nx_int = 15
127const NX_ARC_STAGE_FIELDS: nx_int = 16
128
129// ===== Arc struct =================================================
130//
131// Top-level arc. Owns the stages buffer + pools buffer. Compact;
132// references everything by pointer + count.
133
134struct Arc {
135 arc_id: nx_int,
136 n_stages: nx_int,
137 source: nx_int, // NX_AS_AUTHORED / DYNAMIC / ...
138 default_domain: nx_int, // NX_CD_* default content domain
139 n_pool_ids: nx_int, // total pool-ID count in pools[]
140 stages: *i64, // n_stages * NX_ARC_STAGE_FIELDS
141 pools: *i64 // n_pool_ids
142}
143
144const NX_ARC_BYTES: nx_int = 56 // 7 fields * 8
145
146// ===== Builder ====================================================
147//
148// Allocate an Arc with empty stages + pools buffers sized for the
149// caller's n_stages and n_pool_ids. Caller fills via _set helpers.
150
151func nx_arc_alloc(arc_id: nx_int, n_stages: nx_int,
152 source: nx_int, default_domain: nx_int,
153 n_pool_ids: nx_int) -> *Arc {
154 let arc: *Arc = (sys_mmap(NX_ARC_BYTES)) as *Arc
155 arc.arc_id = arc_id
156 arc.n_stages = n_stages
157 arc.source = source
158 arc.default_domain = default_domain
159 arc.n_pool_ids = n_pool_ids
160 // Stages: n_stages * 16 fields * 8 bytes/i64
161 let stages_bytes: nx_int = n_stages * NX_ARC_STAGE_FIELDS * NX_SIZEOF_NX_INT
162 arc.stages = (sys_mmap(stages_bytes)) as *i64
163 // Pools: n_pool_ids * 8 bytes/i64
164 let pools_bytes: nx_int = n_pool_ids * NX_SIZEOF_NX_INT
165 if n_pool_ids > 0 {
166 arc.pools = (sys_mmap(pools_bytes)) as *i64
167 } else {
168 // Allocate a one-slot buffer so caller can safely query;
169 // empty pool indicated by len=0 on every stage anyway.
170 arc.pools = (sys_mmap(NX_SIZEOF_NX_INT)) as *i64
171 }
172 // Init stages to zero (NishiLang's mmap returns zero pages, but
173 // explicit zeroing makes the contract clear).
174 var i: nx_int = 0
175 while i < n_stages * NX_ARC_STAGE_FIELDS {
176 arc.stages[i] = 0
177 i = i + 1
178 }
179 var j: nx_int = 0
180 while j < n_pool_ids {
181 arc.pools[j] = 0
182 j = j + 1
183 }
184 return arc
185}
186
187// ===== Stage field accessors =======================================
188//
189// Read/write one field of a stage by stage index + field offset.
190// Caller must respect bounds; out-of-range returns 0 / no-op.
191
192func nx_arc_stage_get(arc: *Arc, stage_idx: nx_int, field: nx_int) -> nx_int {
193 if stage_idx < 0 { return 0 }
194 if stage_idx >= arc.n_stages { return 0 }
195 if field < 0 { return 0 }
196 if field >= NX_ARC_STAGE_FIELDS { return 0 }
197 return arc.stages[stage_idx * NX_ARC_STAGE_FIELDS + field]
198}
199
200func nx_arc_stage_set(arc: *Arc, stage_idx: nx_int, field: nx_int, value: nx_int) -> nx_int {
201 if stage_idx < 0 { return 0 - 1 }
202 if stage_idx >= arc.n_stages { return 0 - 1 }
203 if field < 0 { return 0 - 1 }
204 if field >= NX_ARC_STAGE_FIELDS { return 0 - 1 }
205 arc.stages[stage_idx * NX_ARC_STAGE_FIELDS + field] = value
206 return 0
207}
208
209// ===== Pool accessors ==============================================
210//
211// Read/write a pool ID by offset into the shared pools buffer.
212// The (offset, len) pair stored in a stage's location/outfit/pose/...
213// fields identifies which slice of pools[] belongs to that stage.
214
215func nx_arc_pool_get(arc: *Arc, idx: nx_int) -> nx_int {
216 if idx < 0 { return 0 }
217 if idx >= arc.n_pool_ids { return 0 }
218 return arc.pools[idx]
219}
220
221func nx_arc_pool_set(arc: *Arc, idx: nx_int, value: nx_int) -> nx_int {
222 if idx < 0 { return 0 - 1 }
223 if idx >= arc.n_pool_ids { return 0 - 1 }
224 arc.pools[idx] = value
225 return 0
226}
227
228// ===== Convenience: scalar stage setters (named for readability) ===
229
230func nx_arc_stage_set_basic(arc: *Arc, stage_idx: nx_int,
231 stage_number: nx_int, awareness: nx_int,
232 emotional_tone: nx_int,
233 min_arousal_q10: nx_int, max_arousal_q10: nx_int,
234 images_before_advance: nx_int) -> nx_int {
235 nx_arc_stage_set(arc, stage_idx, NX_AST_F_STAGE_NUMBER, stage_number)
236 nx_arc_stage_set(arc, stage_idx, NX_AST_F_AWARENESS, awareness)
237 nx_arc_stage_set(arc, stage_idx, NX_AST_F_EMOTIONAL_TONE, emotional_tone)
238 nx_arc_stage_set(arc, stage_idx, NX_AST_F_MIN_AROUSAL_Q10, min_arousal_q10)
239 nx_arc_stage_set(arc, stage_idx, NX_AST_F_MAX_AROUSAL_Q10, max_arousal_q10)
240 nx_arc_stage_set(arc, stage_idx, NX_AST_F_IMAGES_BEFORE_ADVANCE, images_before_advance)
241 return 0
242}
243
244// Wire a pool's (offset, len) onto a stage. pool_kind selects which
245// of the 5 pool-pair fields to write: 0=location, 1=outfit, 2=pose,
246// 3=expression, 4=camera. Returns -1 on invalid kind.
247
248const NX_ARC_POOL_LOCATION: nx_int = 0
249const NX_ARC_POOL_OUTFIT: nx_int = 1
250const NX_ARC_POOL_POSE: nx_int = 2
251const NX_ARC_POOL_EXPRESSION: nx_int = 3
252const NX_ARC_POOL_CAMERA: nx_int = 4
253const NX_ARC_POOL_N_KINDS: nx_int = 5
254
255func nx_arc_pool_kind_is_valid(k: nx_int) -> nx_int {
256 if k < 0 { return 0 }
257 if k >= NX_ARC_POOL_N_KINDS { return 0 }
258 return 1
259}
260
261func nx_arc_stage_set_pool(arc: *Arc, stage_idx: nx_int,
262 pool_kind: nx_int,
263 pool_offset: nx_int, pool_len: nx_int) -> nx_int {
264 if nx_arc_pool_kind_is_valid(pool_kind) == 0 { return 0 - 1 }
265 var off_field: nx_int = 0
266 var len_field: nx_int = 0
267 if pool_kind == NX_ARC_POOL_LOCATION {
268 off_field = NX_AST_F_LOCATION_POOL_OFF
269 len_field = NX_AST_F_LOCATION_POOL_LEN
270 }
271 if pool_kind == NX_ARC_POOL_OUTFIT {
272 off_field = NX_AST_F_OUTFIT_POOL_OFF
273 len_field = NX_AST_F_OUTFIT_POOL_LEN
274 }
275 if pool_kind == NX_ARC_POOL_POSE {
276 off_field = NX_AST_F_POSE_POOL_OFF
277 len_field = NX_AST_F_POSE_POOL_LEN
278 }
279 if pool_kind == NX_ARC_POOL_EXPRESSION {
280 off_field = NX_AST_F_EXPRESSION_POOL_OFF
281 len_field = NX_AST_F_EXPRESSION_POOL_LEN
282 }
283 if pool_kind == NX_ARC_POOL_CAMERA {
284 off_field = NX_AST_F_CAMERA_POOL_OFF
285 len_field = NX_AST_F_CAMERA_POOL_LEN
286 }
287 nx_arc_stage_set(arc, stage_idx, off_field, pool_offset)
288 nx_arc_stage_set(arc, stage_idx, len_field, pool_len)
289 return 0
290}
291
292// Read the (offset, len) pair for a given stage + pool kind. Sentinel
293// writeback: pool_off and pool_len are returned via caller-supplied
294// *i64 buffers so the function stays <= 8 args.
295
296func nx_arc_stage_get_pool(arc: *Arc, stage_idx: nx_int, pool_kind: nx_int,
297 out_off: *i64, out_len: *i64) -> nx_int {
298 if nx_arc_pool_kind_is_valid(pool_kind) == 0 { return 0 - 1 }
299 var off_field: nx_int = 0
300 var len_field: nx_int = 0
301 if pool_kind == NX_ARC_POOL_LOCATION {
302 off_field = NX_AST_F_LOCATION_POOL_OFF
303 len_field = NX_AST_F_LOCATION_POOL_LEN
304 }
305 if pool_kind == NX_ARC_POOL_OUTFIT {
306 off_field = NX_AST_F_OUTFIT_POOL_OFF
307 len_field = NX_AST_F_OUTFIT_POOL_LEN
308 }
309 if pool_kind == NX_ARC_POOL_POSE {
310 off_field = NX_AST_F_POSE_POOL_OFF
311 len_field = NX_AST_F_POSE_POOL_LEN
312 }
313 if pool_kind == NX_ARC_POOL_EXPRESSION {
314 off_field = NX_AST_F_EXPRESSION_POOL_OFF
315 len_field = NX_AST_F_EXPRESSION_POOL_LEN
316 }
317 if pool_kind == NX_ARC_POOL_CAMERA {
318 off_field = NX_AST_F_CAMERA_POOL_OFF
319 len_field = NX_AST_F_CAMERA_POOL_LEN
320 }
321 out_off[0] = nx_arc_stage_get(arc, stage_idx, off_field)
322 out_len[0] = nx_arc_stage_get(arc, stage_idx, len_field)
323 return 0
324}