nx_aerobic.nx source
↩ module page · 175 lines · 6050 B
1// nx_aerobic.nx -- clean decomposition pile (regenerative metaphor).
2//
3// Per [[feedback-regenerative-stewardship-doctrine-captain-moroni]]:
4// "aerobic composting decomposes cleanly with oxygen + transparency;
5// anaerobic produces methane + smells." Substrate equivalent: when
6// stale data / expired cells / unused capabilities need to be
7// retired, do it AEROBICALLY -- with proper journal entries, content-
8// addressed receipts, operator visibility. Never silent removal.
9//
10// CRITICAL DISCIPLINE: this is the OPPOSITE of nx_lysosome. Lysosome
11// is for FOREIGN material that should be recycled at speed (cleanup
12// queue). Aerobic is for SUBSTRATE-SELF material that has aged out
13// and needs to be released with stewardship.
14//
15// Composes:
16// nx_evict_journal -- every decomposition logged
17// nx_provenance_chain -- each decomposition is a transform
18// nx_chromatin -- germline preserved before aerobic release
19// (so future cells can re-derive)
20// nx_microbiome -- stagnant microbiome (low diversity) +
21// aerobic compost = renewal signal
22//
23// V1 ships:
24// - struct NxAerobicPile of decomposable items
25// - add (mark for release with reason)
26// - check_stagnant (no add/drain in window = anaerobic risk)
27// - drain (caller frees + receives report)
28// - emit_compost_report (summary of what decomposed + when)
29
30import "nx_syscalls.nx"
31import "nx_tier.nx"
32
33// ===== Sealed enum: NxAerobicReason ===============================
34
35const NX_AR_EXPIRED: nx_int = 0 // cell/data aged out
36const NX_AR_SUPERSEDED: nx_int = 1 // newer version exists
37const NX_AR_USER_REQUESTED: nx_int = 2 // operator marked for release
38const NX_AR_UNUSED_FOR_WINDOW: nx_int = 3 // no touches in N seconds
39const NX_AR_VERSION_RETIRED: nx_int = 4 // substrate ABI retired
40const NX_AR_N_REASONS: nx_int = 5
41
42const NX_AE_OK: nx_int = 0
43const NX_AE_ERR_FULL: nx_int = 1
44const NX_AE_ERR_EMPTY: nx_int = 2
45const NX_AE_ERR_BAD_REASON: nx_int = 3
46
47// ===== Struct: NxAerobicItem ======================================
48
49struct NxAerobicItem {
50 item_id: nx_int,
51 content_hash: nx_size,
52 added_us: nx_size,
53 reason: nx_int,
54 drained: nx_int,
55 drained_us: nx_size,
56}
57
58struct NxAerobicPile {
59 items: *NxAerobicItem,
60 capacity: nx_size,
61 head: nx_size, // next slot to write
62 count: nx_size,
63 last_activity_us: nx_size,
64 total_decomposed: nx_int,
65}
66
67const NX_AE_ITEM_BYTES: nx_size = 48
68const NX_AE_STAGNANT_WINDOW_US: nx_size = 86400000000 // 24h default
69
70func nx_ar_is_valid(r: nx_int) -> nx_int {
71 if r < 0 { return 0 }
72 if r >= NX_AR_N_REASONS { return 0 }
73 return 1
74}
75
76func nx_aerobic_pile_new(capacity: nx_size, now_us: nx_size) -> *NxAerobicPile {
77 let p: *NxAerobicPile = (sys_mmap(48)) as *NxAerobicPile
78 let bytes: nx_size = capacity * NX_AE_ITEM_BYTES
79 p.items = (sys_mmap(bytes)) as *NxAerobicItem
80 p.capacity = capacity
81 p.head = 0
82 p.count = 0
83 p.last_activity_us = now_us
84 p.total_decomposed = 0
85 return p
86}
87
88func _ae_at(p: *NxAerobicPile, idx: nx_size) -> *NxAerobicItem {
89 return (p.items as i64 + (idx as i64) * NX_AE_ITEM_BYTES) as *NxAerobicItem
90}
91
92func nx_aerobic_add(p: *NxAerobicPile,
93 item_id: nx_int,
94 content_hash: nx_size,
95 reason: nx_int,
96 now_us: nx_size) -> nx_int {
97 if nx_ar_is_valid(reason) == 0 { return NX_AE_ERR_BAD_REASON }
98 if p.count >= p.capacity { return NX_AE_ERR_FULL }
99 let it: *NxAerobicItem = _ae_at(p, p.head)
100 it.item_id = item_id
101 it.content_hash = content_hash
102 it.added_us = now_us
103 it.reason = reason
104 it.drained = 0
105 it.drained_us = 0
106 p.head = p.head + 1
107 if p.head >= p.capacity { p.head = 0 }
108 p.count = p.count + 1
109 p.last_activity_us = now_us
110 return NX_AE_OK
111}
112
113// ===== nx_aerobic_drain_one =======================================
114//
115// Mark the oldest un-drained item as drained and return its id +
116// content_hash (via out pointers). Caller is responsible for the
117// actual underlying free (vacuole delete, brane revoke, etc.).
118
119func nx_aerobic_drain_one(p: *NxAerobicPile,
120 out_id: *i64,
121 out_hash: *i64,
122 now_us: nx_size) -> nx_int {
123 var i: nx_size = 0
124 while i < p.capacity {
125 let it: *NxAerobicItem = _ae_at(p, i)
126 if it.item_id != 0 {
127 if it.drained == 0 {
128 it.drained = 1
129 it.drained_us = now_us
130 out_id[0] = it.item_id as i64
131 out_hash[0] = it.content_hash as i64
132 p.total_decomposed = p.total_decomposed + 1
133 p.last_activity_us = now_us
134 return NX_AE_OK
135 }
136 }
137 i = i + 1
138 }
139 return NX_AE_ERR_EMPTY
140}
141
142// ===== nx_aerobic_is_stagnant ====================================
143//
144// Predicate: no activity (add or drain) within stagnant_window_us.
145// A pile that's stagnant means decomposition has stopped -- the
146// substrate is failing its stewardship discipline. Operator should
147// investigate.
148
149func nx_aerobic_is_stagnant(p: *NxAerobicPile, now_us: nx_size) -> nx_int {
150 if now_us < p.last_activity_us { return 0 }
151 let elapsed: nx_size = now_us - p.last_activity_us
152 if elapsed > NX_AE_STAGNANT_WINDOW_US { return 1 }
153 return 0
154}
155
156func nx_aerobic_count(p: *NxAerobicPile) -> nx_size {
157 return p.count
158}
159
160func nx_aerobic_total_decomposed(p: *NxAerobicPile) -> nx_int {
161 return p.total_decomposed
162}
163
164func nx_aerobic_count_by_reason(p: *NxAerobicPile, reason: nx_int) -> nx_int {
165 var hits: nx_int = 0
166 var i: nx_size = 0
167 while i < p.capacity {
168 let it: *NxAerobicItem = _ae_at(p, i)
169 if it.item_id != 0 {
170 if it.reason == reason { hits = hits + 1 }
171 }
172 i = i + 1
173 }
174 return hits
175}