nx_greeting.nx source
↩ module page · 138 lines · 6472 B
1// nx_greeting.nx -- WRITING arc, rung W-GR-1: THE THREE-BEAT GREETING.
2//
3// Closes gap-queue row #4 of nx_writebench (pop 342/378). The ruler (arXiv:2601.14324) finds the
4// greeting is THE LOAD-BEARING ARTIFACT of a character bot: it is the only text every user reads,
5// it sets the register, and it is what makes an opening feel like a scene instead of a bio. Its
6// structure is three ordered beats:
7// 1. SELF-INTRODUCTION -- who is speaking
8// 2. SCENARIO / INCITING INCIDENT -- the situation that has already started
9// 3. DIRECT ADDRESS -- the turn to the reader that hands them the move
10//
11// ★WHY ORDER IS PART OF THE MEASUREMENT AND NOT A STYLE OPINION. The same three beats in a
12// different order produce a different artifact: address-first is a chatbot prompt, scenario-first
13// with no introduction is a narrator, and introduction-only is a character sheet. The ruler's
14// finding is about a SEQUENCE, so a detector that only counted presence would call all four of
15// those "complete" and measure nothing. Presence and order are reported separately, and the
16// headline requires both.
17//
18// ★CRAFT, NOT COMPLIANCE. This organ scores STRUCTURE. It has no view on register or content: an
19// SFW greeting and an explicit one are graded by the same three beats, because the thing being
20// measured is whether the opening works, not what it is about. Register lives in nx_register and
21// the legal line lives upstream in rg_decision -- see [[feedback-hosting-legal-posture]].
22//
23// Lexicons stay pure operator DATA (the packed NUL-terminated terms + parallel weights contract
24// nx_register/nx_dialogue/nx_realperson already use), so a lane can tune or translate the markers
25// without touching this logic, and this organ authors no content.
26//
27// Pure integer, NO syscalls, caller owns every buffer.
28// license_tier: ORIGINAL
29// module: nishi-core.write.greeting
30// capability: WRITE_GREETING_STRUCTURE
31
32const GR_BEAT_INTRO: i64 = 0
33const GR_BEAT_SCENARIO: i64 = 1
34const GR_BEAT_ADDRESS: i64 = 2
35const GR_NOT_FOUND: i64 = 0 - 1
36
37// verdicts
38const GR_FLAT: i64 = 0 // fewer than two beats: a bio or a line of dialogue, not a greeting
39const GR_PARTIAL: i64 = 1 // two beats, or three out of canonical order
40const GR_COMPLETE: i64 = 2 // all three, in order
41
42func gr_lower(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
43func gr_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
44func gr_is_wordch(c: i64) -> i64 {
45 if c >= 97 { if c <= 122 { return 1 } }
46 if c >= 65 { if c <= 90 { return 1 } }
47 if c >= 48 { if c <= 57 { return 1 } }
48 if c == 39 { return 1 } // apostrophe: "I'm" is one token
49 return 0
50}
51
52// FIRST position of a whole-word, case-insensitive match, or GR_NOT_FOUND.
53// ★POSITION, NOT JUST PRESENCE -- the order check is the whole point, so the primitive has to
54// return WHERE, and it must not lose that index while searching (the banked parser law).
55func gr_find_word(buf: *u8, a: i64, b: i64, term: *u8) -> i64 {
56 let m: i64 = gr_len(term)
57 if m == 0 { return GR_NOT_FOUND }
58 if b - a < m { return GR_NOT_FOUND }
59 var i: i64 = a
60 while i + m <= b {
61 var j: i64 = 0
62 var same: i64 = 1
63 while j < m {
64 if gr_lower(buf[i + j] as i64) != gr_lower(term[j] as i64) { same = 0; j = m } else { j = j + 1 }
65 }
66 if same == 1 {
67 var lok: i64 = 1
68 var rok: i64 = 1
69 if i > a { if gr_is_wordch(buf[i - 1] as i64) == 1 { lok = 0 } }
70 if i + m < b { if gr_is_wordch(buf[i + m] as i64) == 1 { rok = 0 } }
71 if lok == 1 { if rok == 1 { return i } }
72 }
73 i = i + 1
74 }
75 return GR_NOT_FOUND
76}
77
78// earliest position at which ANY packed term appears, or GR_NOT_FOUND.
79func gr_beat_pos(buf: *u8, a: i64, b: i64, terms: *u8, nterms: i64) -> i64 {
80 var best: i64 = GR_NOT_FOUND
81 var ti: i64 = 0
82 var off: i64 = 0
83 while ti < nterms {
84 let t: *u8 = ((terms as i64) + off) as *u8
85 let tl: i64 = gr_len(t)
86 if tl == 0 { ti = nterms } else {
87 let p: i64 = gr_find_word(buf, a, b, t)
88 if p != GR_NOT_FOUND { if best == GR_NOT_FOUND { best = p } else { if p < best { best = p } } }
89 off = off + tl + 1
90 ti = ti + 1
91 }
92 }
93 return best
94}
95
96// how many of the three beats are present at all
97func gr_beats_present(pi: i64, ps: i64, pa: i64) -> i64 {
98 var n: i64 = 0
99 if pi != GR_NOT_FOUND { n = n + 1 }
100 if ps != GR_NOT_FOUND { n = n + 1 }
101 if pa != GR_NOT_FOUND { n = n + 1 }
102 return n
103}
104
105// are the three first-occurrences in canonical order? 1 = yes. Only meaningful when all three exist.
106func gr_ordered(pi: i64, ps: i64, pa: i64) -> i64 {
107 if pi == GR_NOT_FOUND { return 0 }
108 if ps == GR_NOT_FOUND { return 0 }
109 if pa == GR_NOT_FOUND { return 0 }
110 if pi < ps { if ps < pa { return 1 } }
111 return 0
112}
113
114// THE VERDICT. Presence alone is never COMPLETE -- see the order note in the header.
115func gr_verdict(pi: i64, ps: i64, pa: i64) -> i64 {
116 let n: i64 = gr_beats_present(pi, ps, pa)
117 if n < 2 { return GR_FLAT }
118 if n == 3 { if gr_ordered(pi, ps, pa) == 1 { return GR_COMPLETE } }
119 return GR_PARTIAL
120}
121
122// permil score for a board: beats carry 2/3 of the weight, order the remaining 1/3, so a well-formed
123// but scrambled greeting still scores well above a one-beat stub without ever reaching COMPLETE.
124const GR_PERMIL: i64 = 1000
125// ⚠INTEGER TRUNCATION MADE A PERFECT GREETING SCORE 999. (3*1000*2)/9 = 666 and 1000/3 = 333, so the
126// best possible artifact came out one permil short of the scale it is measured on. That is a small
127// lie with a real cost: every board would print 999 for a flawless opening and someone would
128// eventually spend a cycle hunting the missing permil, or worse, "fix" the greeting.
129// ★A SCORING FUNCTION WHOSE MAXIMUM IS NOT ITS STATED MAXIMUM IS A DEFECT, NOT A ROUNDING DETAIL.
130// COMPLETE is defined as all three beats in order, so it returns the full scale exactly; the
131// partial ladder keeps the truncating form, where losing a permil changes nothing.
132func gr_score_permil(pi: i64, ps: i64, pa: i64) -> i64 {
133 if gr_ordered(pi, ps, pa) == 1 { return GR_PERMIL }
134 let n: i64 = gr_beats_present(pi, ps, pa)
135 var s: i64 = (n * GR_PERMIL * 2) / 9 // 0 / 222 / 444 / 666
136 if s > GR_PERMIL { s = GR_PERMIL }
137 return s
138}