nx_emit_journal.nx source
↩ module page · 144 lines · 5209 B
1// nx_emit_journal.nx -- append-only audit log of substrate emits.
2//
3// Per user oversight directive 2026-05-14 (gap #5): every successful
4// emit must journal who/what/when/where it came from. This is the
5// foundation for the patent-check (#3), quality-gate (#6), and
6// uniqueness-ledger (#8) work that builds on top.
7//
8// One row per emit, in JSONL. No reads, no rotation, no rewrites --
9// append-only by design. Caller picks the path (e.g.,
10// nxc2/_journal/YYYY-MM-DD.jsonl).
11//
12// Row schema:
13// {"module":"nx_quad","source_id":"euclid_elements",
14// "license_class":"pd_classical","ts_ns":<i64>,"event":"emit"}
15//
16// genealogy_id: substrate_audit_journal_2026_05_14
17// lineage_id: append_only_log
18
19// nx_safety_envelope:
20// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
21// sil_target: SIL1
22// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
23// verdict: NOT_YET_EVALUATED
24
25import "nx_syscalls.nx"
26import "nx_runtime.nx"
27import "nx_tier.nx"
28import "nx_fcntl.nx"
29import "nx_clock.nx"
30import "nx_str.nx"
31import "nx_strconv.nx"
32
33const NX_JOURNAL_OK: nx_int = 0
34const NX_JOURNAL_OPEN_FAIL: nx_int = -1
35const NX_JOURNAL_WRITE_FAIL: nx_int = -2
36const NX_JOURNAL_CLOSED: nx_int = -3
37
38const NX_JOURNAL_ROW_CAP: nx_size = 1024
39
40struct NxJournal {
41 fd: nx_fd,
42 n_appended: nx_int,
43 closed: nx_int,
44}
45
46const NX_JOURNAL_STRUCT_BYTES: nx_size = 24
47
48// Open the journal in O_CREAT|O_WRONLY|O_APPEND mode (0x441). Mode 0644.
49// If the file exists, new rows append to the end.
50func nx_journal_open(path: *u8) -> *NxJournal {
51 let raw: *u8 = sys_mmap(NX_JOURNAL_STRUCT_BYTES)
52 let j: *NxJournal = raw as *NxJournal
53 let fd: nx_fd = __syscall(SYS_OPENAT, AT_FDCWD, path as i64, 0x441, 0x1A4, 0, 0)
54 if fd < 0 { return 0 as *NxJournal }
55 j.fd = fd
56 j.n_appended = 0
57 j.closed = 0
58 return j
59}
60
61func nx_journal_close(j: *NxJournal) -> nx_int {
62 if j.closed == 1 { return NX_JOURNAL_OK }
63 if j.fd >= 0 { sys_close(j.fd) }
64 j.fd = -1
65 j.closed = 1
66 return NX_JOURNAL_OK
67}
68
69// Map license-class int back to a stable string for the JSON value.
70func nx_journal_lc_str(lc: nx_int, out: *u8) -> nx_size {
71 if lc == 0 { return nx_str_cpy(out, "unknown" as *u8) }
72 if lc == 1 { return nx_str_cpy(out, "forbidden" as *u8) }
73 if lc == 2 { return nx_str_cpy(out, "pd_classical" as *u8) }
74 if lc == 3 { return nx_str_cpy(out, "pd_explicit" as *u8) }
75 if lc == 4 { return nx_str_cpy(out, "pd_govdoc" as *u8) }
76 if lc == 5 { return nx_str_cpy(out, "academic_open" as *u8) }
77 if lc == 6 { return nx_str_cpy(out, "expired_patent" as *u8) }
78 return nx_str_cpy(out, "unrecognized" as *u8)
79}
80
81// Write all `n` bytes (handles partial writes); returns bytes written.
82func nx_journal_write_all(fd: nx_fd, buf: *u8, n: nx_size) -> nx_size {
83 var w: nx_size = 0
84 while w < n {
85 let p: *u8 = ((buf as nx_size) + w) as *u8
86 let r: nx_int = sys_write(fd, p, n - w)
87 if r <= 0 { return w }
88 w = w + r
89 }
90 return w
91}
92
93// Append one event row to the journal. Returns NX_JOURNAL_OK or a
94// negative status. Captures wall-clock timestamp at write time.
95func nx_journal_append(
96 j: *NxJournal,
97 module: *u8,
98 source_id: *u8,
99 license_class: nx_int,
100 event: *u8
101) -> nx_int {
102 if j.closed == 1 { return NX_JOURNAL_CLOSED }
103 let row_buf: *u8 = sys_mmap(NX_JOURNAL_ROW_CAP)
104 let lc_buf: *u8 = sys_mmap(32)
105 let ts_buf: *u8 = sys_mmap(32)
106 nx_journal_lc_str(license_class, lc_buf)
107 let ts: nx_int = nx_clock_realtime_ns()
108 let ts_len: nx_int = nx_strconv_format_i64(ts, ts_buf)
109
110 // row_buf is freshly mmap'd (zero-filled), so nx_str_cat's
111 // length-find on each call starts at 0 and advances correctly.
112 // nx_str.nx's nx_str_cat is (dst, src) -> dst; it finds the
113 // current length internally.
114 row_buf[0] = 0
115 nx_str_cat(row_buf, "{\"module\":\"" as *u8)
116 nx_str_cat(row_buf, module)
117 nx_str_cat(row_buf, "\",\"source_id\":\"" as *u8)
118 nx_str_cat(row_buf, source_id)
119 nx_str_cat(row_buf, "\",\"license_class\":\"" as *u8)
120 nx_str_cat(row_buf, lc_buf)
121 nx_str_cat(row_buf, "\",\"ts_ns\":" as *u8)
122 nx_str_cat(row_buf, ts_buf)
123 nx_str_cat(row_buf, ",\"event\":\"" as *u8)
124 nx_str_cat(row_buf, event)
125 nx_str_cat(row_buf, "\"}\n" as *u8)
126
127 let total: nx_size = nx_str_len(row_buf) as nx_size
128 let w: nx_size = nx_journal_write_all(j.fd, row_buf, total)
129 if w != total { return NX_JOURNAL_WRITE_FAIL }
130 j.n_appended = j.n_appended + 1
131 return NX_JOURNAL_OK
132}
133
134// Convenience: append an "emit" event (most common case).
135func nx_journal_emit(j: *NxJournal, module: *u8, source_id: *u8, license_class: nx_int) -> nx_int {
136 return nx_journal_append(j, module, source_id, license_class, "emit" as *u8)
137}
138
139// Convenience: append a "refused" event (gate blocked the emit).
140func nx_journal_refuse(j: *NxJournal, module: *u8, source_id: *u8, license_class: nx_int) -> nx_int {
141 return nx_journal_append(j, module, source_id, license_class, "refused" as *u8)
142}
143
144func nx_journal_n_appended(j: *NxJournal) -> nx_int { return j.n_appended }