nx_object_spec_match.nx source
↩ module page · 335 lines · 11771 B
1// nx_object_spec_match.nx -- spec-compliance verification.
2//
3// CAPABILITY_COMPLETENESS: FULL
4//
5// Composes:
6// nx_object_spec (the catalog primitive)
7// caller-provided observed_parts list (upstream: nx_part_
8// classifier, queued)
9//
10// Algorithm:
11// 1. For each REQUIRED part in spec: check if in observed list
12// 2. For each NEGATION in spec: check if in observed list
13// 3. Classify:
14// all required present + no negations -> MATCHES_SPEC
15// any negation present -> WRONG_CLASS
16// some required missing + no negations -> PART_MISSING
17// both required missing + negations -> PARTIAL_MATCH
18// 4. If WRONG_CLASS or PARTIAL_MATCH: scan all other specs for
19// the best-matching alternative (highest required-presence
20// score + no negations violated)
21//
22// genealogy_id: substrate_object_spec_match_2026_05_16
23// lineage_id: nx_object_spec_match_v1
24
25import "nx_syscalls.nx"
26import "nx_runtime.nx"
27import "nx_tier.nx"
28import "nx_object_spec.nx"
29const NX_MAGIC_1024: i64 = 1024
30
31// ===== sealed-verdict outcomes ===================================
32
33const NX_OSM_VERDICT_MATCHES_SPEC: nx_int = 0
34const NX_OSM_VERDICT_PART_MISSING: nx_int = 1
35const NX_OSM_VERDICT_WRONG_CLASS: nx_int = 2
36const NX_OSM_VERDICT_PARTIAL_MATCH: nx_int = 3
37const NX_OSM_VERDICT_NO_OBSERVATION: nx_int = 4
38
39const NX_OSM_NO_ALTERNATIVE: nx_int = -1
40
41// ===== match result struct =======================================
42
43struct NxObjectMatchResult {
44 spec_id: nx_int,
45 verdict: nx_int,
46 n_required_matched: nx_int,
47 n_required_missing: nx_int,
48 n_negations_violated: nx_int,
49 missing_parts: *nx_int,
50 n_missing: nx_int,
51 violated_negations: *nx_int,
52 n_violated: nx_int,
53 suggested_alternative: nx_int, // spec_id of closest match if WRONG_CLASS
54 match_confidence_q10: nx_int, // n_matched / n_required (Q10)
55}
56
57const NX_OSM_RESULT_BYTES: nx_size = 96
58
59// ===== helpers ====================================================
60
61func _osm_in_list(list: *nx_int, n: nx_int, val: nx_int) -> nx_int {
62 var i: nx_int = 0
63 while i < n {
64 if list[i] == val { return 1 }
65 i = i + 1
66 }
67 return 0
68}
69
70// ===== best-alternative finder ===================================
71//
72// Score for each candidate spec:
73// +1 per required-part observed
74// -2 per negation observed (heavy penalty if candidate spec
75// forbids one of the observed parts)
76// alternative is candidate with highest score above 0 +
77// zero negations violated
78//
79// Returns spec_id of best alternative, or NO_ALTERNATIVE if
80// no candidate scores positive.
81
82func _osm_find_alternative(
83 excluded_spec_id: nx_int,
84 observed_parts: *nx_int, n_observed: nx_int) -> nx_int {
85
86 var best_spec: nx_int = NX_OSM_NO_ALTERNATIVE
87 var best_score: nx_int = 0
88 var s: nx_int = 0
89 while s < NX_SPEC_N_SPECS {
90 if s != excluded_spec_id {
91 let candidate: *NxObjectSpec = nx_object_spec_get(s)
92 if candidate != (0 as *NxObjectSpec) {
93 // Count required matches.
94 var score: nx_int = 0
95 var has_violation: nx_int = 0
96 var i: nx_int = 0
97 while i < candidate.n_required {
98 let pid: nx_int = candidate.required_parts[i]
99 if _osm_in_list(observed_parts, n_observed, pid) == 1 {
100 score = score + 1
101 }
102 i = i + 1
103 }
104 var j: nx_int = 0
105 while j < candidate.n_negations {
106 let neg_pid: nx_int = candidate.negation_parts[j]
107 if _osm_in_list(observed_parts, n_observed, neg_pid) == 1 {
108 has_violation = 1
109 }
110 j = j + 1
111 }
112 if has_violation == 0 {
113 if score > best_score {
114 best_score = score
115 best_spec = s
116 }
117 }
118 }
119 }
120 s = s + 1
121 }
122 return best_spec
123}
124
125// ===== top-level match ===========================================
126
127func nx_object_spec_match(
128 spec_id: nx_int,
129 observed_parts: *nx_int, n_observed: nx_int) -> *NxObjectMatchResult {
130
131 let r_ptr: *u8 = sys_mmap(NX_OSM_RESULT_BYTES)
132 let r: *NxObjectMatchResult = r_ptr as *NxObjectMatchResult
133 r.spec_id = spec_id
134 r.n_required_matched = 0
135 r.n_required_missing = 0
136 r.n_negations_violated = 0
137 r.n_missing = 0
138 r.n_violated = 0
139 r.suggested_alternative = NX_OSM_NO_ALTERNATIVE
140 r.match_confidence_q10 = 0
141
142 let spec: *NxObjectSpec = nx_object_spec_get(spec_id)
143 if spec == (0 as *NxObjectSpec) {
144 r.verdict = NX_OSM_VERDICT_NO_OBSERVATION
145 return r
146 }
147 if n_observed <= 0 {
148 r.verdict = NX_OSM_VERDICT_NO_OBSERVATION
149 return r
150 }
151
152 // Allocate output lists.
153 let missing_bytes: nx_size = (spec.n_required as nx_size) * 8
154 let missing: *nx_int = (sys_mmap(missing_bytes)) as *nx_int
155 var miss_n: nx_int = 0
156 let violated_bytes: nx_size = (spec.n_negations as nx_size) * 8
157 let violated: *nx_int = (sys_mmap(violated_bytes)) as *nx_int
158 var viol_n: nx_int = 0
159
160 // 1. Required-part scan.
161 var n_matched: nx_int = 0
162 var i: nx_int = 0
163 while i < spec.n_required {
164 let pid: nx_int = spec.required_parts[i]
165 if _osm_in_list(observed_parts, n_observed, pid) == 1 {
166 n_matched = n_matched + 1
167 } else {
168 missing[miss_n] = pid
169 miss_n = miss_n + 1
170 }
171 i = i + 1
172 }
173
174 // 2. Negation scan.
175 var j: nx_int = 0
176 while j < spec.n_negations {
177 let neg_pid: nx_int = spec.negation_parts[j]
178 if _osm_in_list(observed_parts, n_observed, neg_pid) == 1 {
179 violated[viol_n] = neg_pid
180 viol_n = viol_n + 1
181 }
182 j = j + 1
183 }
184
185 r.n_required_matched = n_matched
186 r.n_required_missing = miss_n
187 r.n_negations_violated = viol_n
188 r.missing_parts = missing
189 r.n_missing = miss_n
190 r.violated_negations = violated
191 r.n_violated = viol_n
192 if spec.n_required > 0 {
193 r.match_confidence_q10 = (n_matched * NX_MAGIC_1024) / spec.n_required
194 } else {
195 r.match_confidence_q10 = NX_MAGIC_1024
196 }
197
198 // 3. Classify.
199 if viol_n == 0 {
200 if miss_n == 0 {
201 r.verdict = NX_OSM_VERDICT_MATCHES_SPEC
202 return r
203 }
204 r.verdict = NX_OSM_VERDICT_PART_MISSING
205 return r
206 }
207 if miss_n == 0 {
208 r.verdict = NX_OSM_VERDICT_WRONG_CLASS
209 } else {
210 r.verdict = NX_OSM_VERDICT_PARTIAL_MATCH
211 }
212
213 // 4. Find best alternative for WRONG_CLASS / PARTIAL_MATCH.
214 let alt: nx_int = _osm_find_alternative(spec_id, observed_parts, n_observed)
215 r.suggested_alternative = alt
216 return r
217}
218
219// ===== self-test =================================================
220
221func main() -> nx_int {
222 // ---- CASE 1: D&D sword that IS a sword
223 //
224 // Observed: BLADE, EDGE, CROSSGUARD, GRIP, POMMEL, POINT
225 // Spec: SWORD_LONG (requires BLADE/EDGE/CROSSGUARD/GRIP/POMMEL)
226 // Expected: MATCHES_SPEC
227 let obs1: *nx_int = (sys_mmap(64)) as *nx_int
228 obs1[0] = NX_PT_BLADE
229 obs1[1] = NX_PT_EDGE
230 obs1[2] = NX_PT_CROSSGUARD
231 obs1[3] = NX_PT_GRIP
232 obs1[4] = NX_PT_POMMEL
233 obs1[5] = NX_PT_POINT
234 let r1: *NxObjectMatchResult = nx_object_spec_match(
235 NX_SPEC_SWORD_LONG, obs1, 6)
236 if r1 == (0 as *NxObjectMatchResult) { return 1 }
237 if r1.verdict != NX_OSM_VERDICT_MATCHES_SPEC { return 2 }
238 if r1.n_required_matched != 5 { return 3 }
239 if r1.n_negations_violated != 0 { return 4 }
240 if r1.match_confidence_q10 != NX_MAGIC_1024 { return 5 }
241
242 // ---- CASE 2: "DnD sword" that's actually a bubble wand
243 //
244 // Observed: HANDLE_STICK, LOOP, FILM
245 // Spec: SWORD_LONG
246 // Expected: WRONG_CLASS (LOOP + FILM are sword negations) +
247 // PARTIAL_MATCH actually since required parts are
248 // also missing. Both negations violated AND
249 // required missing -> PARTIAL_MATCH.
250 let obs2: *nx_int = (sys_mmap(64)) as *nx_int
251 obs2[0] = NX_PT_HANDLE_STICK
252 obs2[1] = NX_PT_LOOP
253 obs2[2] = NX_PT_FILM
254 let r2: *NxObjectMatchResult = nx_object_spec_match(
255 NX_SPEC_SWORD_LONG, obs2, 3)
256 if r2.verdict != NX_OSM_VERDICT_PARTIAL_MATCH { return 10 }
257 if r2.n_negations_violated < 2 { return 11 }
258 if r2.n_required_missing != 5 { return 12 }
259 // The suggested alternative should be BUBBLE_WAND (it has
260 // HANDLE_STICK + LOOP required and FILM optional, no violations).
261 if r2.suggested_alternative != NX_SPEC_BUBBLE_WAND { return 13 }
262
263 // ---- CASE 3: ORC that IS an orc
264 //
265 // Observed: HEAD, TUSK, FANG
266 // Spec: ORC_STANDARD
267 // Expected: MATCHES_SPEC
268 let obs3: *nx_int = (sys_mmap(64)) as *nx_int
269 obs3[0] = NX_PT_HEAD
270 obs3[1] = NX_PT_TUSK
271 obs3[2] = NX_PT_FANG
272 let r3: *NxObjectMatchResult = nx_object_spec_match(
273 NX_SPEC_ORC_STANDARD, obs3, 3)
274 if r3.verdict != NX_OSM_VERDICT_MATCHES_SPEC { return 20 }
275
276 // ---- CASE 4: ORC prompt got an ELF (pointed ear instead of tusks)
277 //
278 // Observed: HEAD, POINTED_EAR
279 // Spec: ORC_STANDARD (negates POINTED_EAR)
280 // Expected: WRONG_CLASS (negation hit + required TUSK missing)
281 // Wait: with TUSK missing AND POINTED_EAR negation
282 // hit, that's PARTIAL_MATCH (both miss + violate).
283 // Suggested alternative: ELF_HIGH
284 let obs4: *nx_int = (sys_mmap(64)) as *nx_int
285 obs4[0] = NX_PT_HEAD
286 obs4[1] = NX_PT_POINTED_EAR
287 let r4: *NxObjectMatchResult = nx_object_spec_match(
288 NX_SPEC_ORC_STANDARD, obs4, 2)
289 if r4.verdict != NX_OSM_VERDICT_PARTIAL_MATCH { return 30 }
290 if r4.suggested_alternative != NX_SPEC_ELF_HIGH { return 31 }
291
292 // ---- CASE 5: PART_MISSING case (required missing, no violation)
293 //
294 // Observed: BLADE, GRIP, POMMEL (no CROSSGUARD, no EDGE)
295 // Spec: SWORD_LONG
296 // Expected: PART_MISSING (no negations hit; some required absent)
297 let obs5: *nx_int = (sys_mmap(64)) as *nx_int
298 obs5[0] = NX_PT_BLADE
299 obs5[1] = NX_PT_GRIP
300 obs5[2] = NX_PT_POMMEL
301 let r5: *NxObjectMatchResult = nx_object_spec_match(
302 NX_SPEC_SWORD_LONG, obs5, 3)
303 if r5.verdict != NX_OSM_VERDICT_PART_MISSING { return 40 }
304 if r5.n_required_missing != 2 { return 41 } // EDGE + CROSSGUARD
305
306 // ---- CASE 6: WRONG_CLASS pure (all required present + negation hit)
307 //
308 // Observed: BLADE, EDGE, CROSSGUARD, GRIP, POMMEL, LOOP
309 // Spec: SWORD_LONG
310 // Expected: WRONG_CLASS (required all present BUT LOOP violates)
311 let obs6: *nx_int = (sys_mmap(64)) as *nx_int
312 obs6[0] = NX_PT_BLADE
313 obs6[1] = NX_PT_EDGE
314 obs6[2] = NX_PT_CROSSGUARD
315 obs6[3] = NX_PT_GRIP
316 obs6[4] = NX_PT_POMMEL
317 obs6[5] = NX_PT_LOOP
318 let r6: *NxObjectMatchResult = nx_object_spec_match(
319 NX_SPEC_SWORD_LONG, obs6, 6)
320 if r6.verdict != NX_OSM_VERDICT_WRONG_CLASS { return 50 }
321 if r6.n_negations_violated < 1 { return 51 }
322
323 // ---- CASE 7: NO_OBSERVATION (empty observed list)
324 let obs7: *nx_int = (sys_mmap(8)) as *nx_int
325 let r7: *NxObjectMatchResult = nx_object_spec_match(
326 NX_SPEC_SWORD_LONG, obs7, 0)
327 if r7.verdict != NX_OSM_VERDICT_NO_OBSERVATION { return 60 }
328
329 // ---- CASE 8: invalid spec_id
330 let r8: *NxObjectMatchResult = nx_object_spec_match(
331 NX_SPEC_N_SPECS, obs1, 5)
332 if r8.verdict != NX_OSM_VERDICT_NO_OBSERVATION { return 70 }
333
334 return 0
335}