nx_handcheck.nx source
↩ module page · 734 lines · 33646 B
1// nx_handcheck.nx -- THE HAND STRUCTURE JUDGE over an NXA SKEL (operator 2026-09-01: "the hands are
2// bad"). It is the SIBLING OF nx_footcheck and deliberately mirrors its contract -- check / selftest,
3// one measured report, one predicate made of things a hand IS rather than a blended score -- because
4// two judges for two ends of the same limb must not have two different shapes.
5//
6// WHY IT EXISTS: the hands were the ONE named character defect in this estate with NO instrument at
7// all. nx_clipcheck measures limb-vs-torso and limb-vs-floor; nx_footcheck measures the distal foot;
8// nothing measured a hand. Grading therefore depended entirely on the operator's eye.
9//
10// THE STRUCTURAL PROBLEM, AND WHY THIS IS NOT NAME MATCHING: NXA's SKEL carries NO JOINT NAMES, by
11// design (nx_nxa_joints.nx states it and quotes nx_nxa_rig_emit: "SKEL carries no names by design").
12// So a hand cannot be looked up -- it must be DERIVED FROM TOPOLOGY. A hand is the only place on a
13// humanoid where three or more TERMINAL SIMPLE CHAINS hang off one attachment. That is what this
14// organ finds. Nothing here knows a joint index, so the judge keeps working when joints are added or
15// reordered ahead of it -- the same structural stance nx_footcheck takes with its rotation.
16//
17// THE ANATOMICAL CANON IS CITED, NOT DIALLED. Buryanov and Kotiuk, "Proportions of Hand Segments",
18// Int. J. Morphol. 28(3):755-758, 2010 (open access; mirror
19// knowledge/fetched/cmp_handcheck_buryanov2010.pdf, sha256 ec2a49be..., 66 adult hands by X-ray with
20// a radiopaque ruler and no scaling error). Their Table I lengths in mm, reconstructed and then
21// INDEPENDENTLY CONFIRMED by their own Table II ratio row for every one of the five rays -- two
22// instruments inside one document agreeing:
23// ray distal medial proximal metacarpal Tab.II ratio (pd:pm:pp:m)
24// I 21.67 -- 31.57 46.22 1 : -- : 1.5 : 2.1
25// II 15.82 22.38 39.78 68.12 1 : 1.4 : 2.5 : 4.3
26// III 17.40 26.33 44.63 64.60 1 : 1.5 : 2.6 : 3.7
27// IV 17.30 25.65 41.37 58.00 1 : 1.5 : 2.4 : 3.4
28// V 15.96 18.11 32.74 53.69 1 : 1.1 : 2.1 : 3.4
29// TWO LAWS FALL OUT OF THAT TABLE AND BOTH ARE ENFORCED HERE:
30// (1) MONOTONIC DISTAL SHORTENING -- proximal > medial > distal holds in ALL FIVE rays without
31// exception. It is dimensionless, so it survives any stylisation of hand size. It is the STRICT
32// clause here and it needs no tolerance at all.
33// (2) PHALANX COUNT -- four fingers carry THREE phalanges and the thumb carries TWO (the paper's
34// medial column is empty for ray I, and its text names "even different phalanges numbers").
35// In a rig a chain of L joints spans L-1 bones, so a finger is a 4-joint chain and a thumb a
36// 3-joint chain.
37//
38// DECLARED IMPRECISION -- name it or the next reader trusts it as exact:
39// - v1 measures the BIND SKELETON. It does NOT read SKIN weights, so it cannot yet say whether the
40// hand is a distinct skinned region or fused to the forearm; and it does NOT read POSE or ANIM, so
41// it says nothing about a hand during motion. Both are named gaps, not silent ones.
42// - digit interpenetration is measured JOINT-TO-JOINT, not mesh-to-mesh. It catches the fused-mitten
43// class (two digits occupying one another) and will miss a thin surface graze.
44// - the up axis is derived as the rig's LONGEST extent, exactly as nx_nxa_joints.nx derives it.
45// THAT IS A DUPLICATE RULER AND IT IS NAMED HERE RATHER THAN LEFT SILENT: nx_nxa_joints is a
46// PROGRAM with a main(), so there is no importable function today. The correct fix is to extract
47// its axis derivation into a lib and have both organs compose it; this organ is written so that
48// swap is one function body.
49// - one intermediate bone between the attachment and the digits (a palm or carpal bone) is merged,
50// so wrist{thumb} plus palm{4 fingers} reads as one five-digit hand. Deeper nesting is NOT merged
51// and is reported as separate hubs rather than guessed at.
52//
53// nx_handcheck check <file.nxa> -> the measured report and verdict for a real asset
54// nx_handcheck dump <file.nxa> -> the joint tree and every digit chain found (measurement aid)
55// nx_handcheck selftest -> gv gate: proves the JUDGE discriminates, on fixtures
56// exit: 0 GREEN | 1 RED | 2 usage | 3 unreadable | 4 UNOBSERVABLE no hand chain (abstain, never pass)
57// license_tier: ORIGINAL expect_exit: 0 No hw writes (Rule 26).
58import "nx_syscalls.nx"
59import "nx_nxa.nx"
60import "nx_gate_verdict.nx"
61
62const HC_WPJ: i64 = 8 // SKEL record: [parent][x][y][z][qx][qy][qz][qw]
63const HC_MAXJ: i64 = 4096
64const HC_MAXD: i64 = 24 // digit chains tracked per hub
65const HC_MAXH: i64 = 8 // hubs tracked
66const HC_MAXCH: i64 = 24 // joints tracked per digit chain
67const HC_Q: i64 = 1000
68const HC_BIG: i64 = 1000000000
69// a digit is a terminal chain of at least this many JOINTS (2 joints = 1 bone = a stub digit).
70const HC_MIN_CHAIN: i64 = 2
71// three terminal chains on one attachment is the smallest thing that is a hand rather than a fork.
72const HC_MIN_HUB_DIGITS: i64 = 3
73// CITED (Buryanov and Kotiuk Tab. I and II): five rays, four of them with three phalanges.
74const HC_DIGITS_CANON: i64 = 5
75const HC_FINGER_BONES: i64 = 3
76const HC_THUMB_BONES: i64 = 2
77const HC_MIN_3BONE: i64 = 4
78// UPPER bounds only. The LOWER bound is monotonicity, which is enforced by its own clause -- one
79// invariant must not get two rulers. Measured envelope: proximal/medial 1613..1808 permil and
80// medial/distal 1135..1513 permil. These ceilings are those maxima widened for stylisation and are
81// PICKED, not derived, so they are wrong in the direction of ACCEPTING a hand rather than rejecting.
82const HC_R_PPPM_MAX: i64 = 2600
83const HC_R_PMPD_MAX: i64 = 2600
84// PICKED, wrong-toward-accept: a thumb opposed by less than this reads as a rake, not a hand.
85// 150 permil of cosine deviation is about 32 degrees; a human thumb sits far past it.
86const HC_THUMB_OPP_MIN: i64 = 150
87// PICKED, wrong-toward-accept: two joints of DIFFERENT digits closer than this fraction of the mean
88// bone length are occupying one another. 100 permil of a mean phalanx is about 3 mm on an adult hand.
89const HC_FUSE_PERMIL: i64 = 100
90// hands sit above the hip; a multi-chain hub below this fraction of rig height is a FOOT, not a hand.
91// Relative to the rig's own extent, so it carries across scales.
92const HC_HAND_MIN_H01: i64 = 150
93const HC_OUT: i64 = 32
94const HC_NEWTON: i64 = 64
95const HC_SQRTCAP: i64 = 65536
96
97func hc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
98func hc_puts(s: *u8) -> i64 { sys_write(1, s, hc_slen(s)); return 0 }
99func hc_pn(v: i64) -> i64 {
100 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
101 var m: i64 = v
102 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
103 let t: *u8 = sys_mmap(32)
104 var k: i64 = 0
105 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
106 let o: *u8 = sys_mmap(32)
107 var i: i64 = 0
108 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
109 sys_write(1, o, k)
110 return 0
111}
112func hc_streq(a: *u8, b: *u8) -> i64 {
113 var i: i64 = 0
114 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
115 if b[i] != (0 as u8) { return 0 }
116 return 1
117}
118// integer square root, Newton then exact correction. Distances here are in the SKEL's own units
119// (0.01 mm on the shipped rigs) and every published figure is a RATIO, so the unit never leaves here.
120func hc_isqrt(v: i64) -> i64 {
121 if v <= 0 { return 0 }
122 var g: i64 = v
123 if g > HC_SQRTCAP { g = HC_SQRTCAP }
124 var i: i64 = 0
125 while i < HC_NEWTON {
126 let ng: i64 = (g + v / g) / 2
127 if ng == g { i = HC_NEWTON } else { g = ng; i = i + 1 }
128 }
129 if g < 1 { g = 1 }
130 while g * g > v { g = g - 1 }
131 while (g + 1) * (g + 1) <= v { g = g + 1 }
132 return g
133}
134func hc_dist(W: *i64, sw: i64, a: i64, b: i64) -> i64 {
135 let pa: i64 = sw + 1 + a * HC_WPJ
136 let pb: i64 = sw + 1 + b * HC_WPJ
137 let dx: i64 = W[pa+1] - W[pb+1]
138 let dy: i64 = W[pa+2] - W[pb+2]
139 let dz: i64 = W[pa+3] - W[pb+3]
140 return hc_isqrt(dx*dx + dy*dy + dz*dz)
141}
142
143// ---- topology --------------------------------------------------------------------------------
144// NC[j] = number of children of j. A parent field outside [0,nj) or equal to j is a ROOT.
145func hc_children(W: *i64, sw: i64, nj: i64, NC: *i64) -> i64 {
146 var j: i64 = 0
147 while j < nj { NC[j] = 0; j = j + 1 }
148 j = 0
149 while j < nj {
150 let p: i64 = W[sw + 1 + j * HC_WPJ]
151 if p >= 0 { if p < nj { if p != j { NC[p] = NC[p] + 1 } } }
152 j = j + 1
153 }
154 return 0
155}
156// the only child of j, or -1. Used to walk a simple path without building an adjacency list.
157func hc_only_child(W: *i64, sw: i64, nj: i64, j: i64) -> i64 {
158 var found: i64 = 0 - 1
159 var c: i64 = 0
160 while c < nj {
161 let p: i64 = W[sw + 1 + c * HC_WPJ]
162 if p == j { if c != j { found = c } }
163 c = c + 1
164 }
165 return found
166}
167// If the subtree rooted at d is a SIMPLE PATH, write its joints into CH and return the joint count.
168// Otherwise return 0 -- a branching subtree is an arm or a spine, never a digit.
169func hc_chain(W: *i64, sw: i64, nj: i64, NC: *i64, d: i64, CH: *i64) -> i64 {
170 var n: i64 = 0
171 var cur: i64 = d
172 var go: i64 = 1
173 while go == 1 {
174 if n >= HC_MAXCH { return 0 }
175 CH[n] = cur
176 n = n + 1
177 if NC[cur] == 0 { go = 0 } else {
178 if NC[cur] > 1 { return 0 }
179 cur = hc_only_child(W, sw, nj, cur)
180 if cur < 0 { return 0 }
181 }
182 }
183 return n
184}
185// count the terminal chains attached to j that are long enough to be a digit
186func hc_hub_digits(W: *i64, sw: i64, nj: i64, NC: *i64, j: i64, CH: *i64) -> i64 {
187 var n: i64 = 0
188 var c: i64 = 0
189 while c < nj {
190 let p: i64 = W[sw + 1 + c * HC_WPJ]
191 if p == j { if c != j {
192 if hc_chain(W, sw, nj, NC, c, CH) >= HC_MIN_CHAIN { n = n + 1 }
193 } }
194 c = c + 1
195 }
196 return n
197}
198
199// ---- the measurement -------------------------------------------------------------------------
200// out[0]=joints [1]=live_hubs [2]=hub_joint [3]=digits [4]=bones_total [5]=digits_3bone
201// out[6]=digits_2bone [7]=mono_violations [8]=fused_pairs [9]=thumb_opp_permil
202// out[10]=taper_violations [11]=hub_h01_permil [12]=mean_bone [13]=up_axis [14]=min_chain
203// out[15]=max_chain [16]=hand_hubs [17]=worst_taper_permil
204// RETURNS 0 measured | -1 no SKEL joints | -2 NO DIGIT HUB AT ALL | -3 hubs exist but none above the
205// hand band (a foot). Every negative is an ABSTENTION, never a pass.
206func hc_measure(W: *i64, sw: i64, nj: i64, out: *i64) -> i64 {
207 var z: i64 = 0
208 while z < HC_OUT { out[z] = 0; z = z + 1 }
209 out[0] = nj
210 if nj <= 0 { return 0 - 1 }
211 if nj > HC_MAXJ { return 0 - 1 }
212 let NC: *i64 = sys_mmap(HC_MAXJ*8) as *i64
213 hc_children(W, sw, nj, NC)
214 // up axis = the rig's LONGEST extent (see the duplicate-ruler note in the header)
215 let mn: *i64 = sys_mmap(32) as *i64
216 let mx: *i64 = sys_mmap(32) as *i64
217 var a: i64 = 0
218 while a < 3 { mn[a] = HC_BIG; mx[a] = 0 - HC_BIG; a = a + 1 }
219 var j: i64 = 0
220 while j < nj {
221 let base: i64 = sw + 1 + j * HC_WPJ
222 a = 0
223 while a < 3 {
224 let v: i64 = W[base + 1 + a]
225 if v < mn[a] { mn[a] = v }
226 if v > mx[a] { mx[a] = v }
227 a = a + 1
228 }
229 j = j + 1
230 }
231 var up: i64 = 0
232 var span: i64 = mx[0] - mn[0]
233 a = 1
234 while a < 3 { if mx[a] - mn[a] > span { span = mx[a] - mn[a]; up = a } a = a + 1 }
235 out[13] = up
236 // hubs: every joint hosting HC_MIN_HUB_DIGITS or more terminal chains
237 let CH: *i64 = sys_mmap(HC_MAXCH*8) as *i64
238 let HUB: *i64 = sys_mmap(HC_MAXH*8) as *i64
239 let HUBN: *i64 = sys_mmap(HC_MAXH*8) as *i64
240 var nhub: i64 = 0
241 j = 0
242 while j < nj {
243 let d: i64 = hc_hub_digits(W, sw, nj, NC, j, CH)
244 if d >= HC_MIN_HUB_DIGITS { if nhub < HC_MAXH { HUB[nhub] = j; HUBN[nhub] = d; nhub = nhub + 1 } }
245 j = j + 1
246 }
247 // ONE-LEVEL MERGE: a hub whose PARENT is also a hub is a palm under a wrist. Fold the child hub
248 // into the parent so wrist{thumb} plus palm{4 fingers} reads as one five-digit hand. Deeper
249 // nesting is deliberately NOT folded -- it is reported as separate hubs rather than guessed at.
250 var h: i64 = 0
251 while h < nhub {
252 let pj: i64 = W[sw + 1 + HUB[h] * HC_WPJ]
253 var k: i64 = 0
254 while k < nhub {
255 if HUB[k] == pj { if HUBN[h] > 0 { HUBN[k] = HUBN[k] + HUBN[h]; HUBN[h] = 0 } }
256 k = k + 1
257 }
258 h = h + 1
259 }
260 var live: i64 = 0
261 h = 0
262 while h < nhub { if HUBN[h] > 0 { live = live + 1 } h = h + 1 }
263 out[1] = live
264 if live == 0 { return 0 - 2 }
265 // pick the hand: the LIVE hub with the most digits that sits above HC_HAND_MIN_H01. Height is
266 // normalised to the rig's own extent, so a toe hub (h01 near zero) can never be graded as a hand.
267 var best: i64 = 0 - 1
268 var bestn: i64 = 0
269 var hands: i64 = 0
270 h = 0
271 while h < nhub {
272 if HUBN[h] > 0 {
273 let y: i64 = W[sw + 1 + HUB[h] * HC_WPJ + 1 + up]
274 var h01: i64 = 0
275 if span > 0 { h01 = (y - mn[up]) * HC_Q / span }
276 if h01 >= HC_HAND_MIN_H01 {
277 hands = hands + 1
278 if HUBN[h] > bestn { bestn = HUBN[h]; best = h; out[11] = h01 }
279 }
280 }
281 h = h + 1
282 }
283 out[16] = hands
284 if best < 0 { return 0 - 3 }
285 let hub: i64 = HUB[best]
286 out[2] = hub
287 // walk every digit on the chosen hub -- and on a child hub if one was folded into it
288 let DR: *i64 = sys_mmap(HC_MAXD*8) as *i64 // digit chain roots
289 let DL: *i64 = sys_mmap(HC_MAXD*8) as *i64 // joints in each chain
290 let DJ: *i64 = sys_mmap(HC_MAXD*HC_MAXCH*8) as *i64 // the joints themselves
291 var nd: i64 = 0
292 var host: i64 = 0
293 while host < nj {
294 var is_host: i64 = 0
295 if host == hub { is_host = 1 }
296 if W[sw + 1 + host * HC_WPJ] == hub {
297 if hc_hub_digits(W, sw, nj, NC, host, CH) >= HC_MIN_HUB_DIGITS { is_host = 1 }
298 }
299 if is_host == 1 {
300 var c: i64 = 0
301 while c < nj {
302 if W[sw + 1 + c * HC_WPJ] == host { if c != host {
303 let L: i64 = hc_chain(W, sw, nj, NC, c, CH)
304 if L >= HC_MIN_CHAIN { if nd < HC_MAXD {
305 DR[nd] = c
306 DL[nd] = L
307 var q: i64 = 0
308 while q < L { DJ[nd*HC_MAXCH + q] = CH[q]; q = q + 1 }
309 nd = nd + 1
310 } }
311 } }
312 c = c + 1
313 }
314 }
315 host = host + 1
316 }
317 out[3] = nd
318 if nd == 0 { return 0 - 2 }
319 // bones, monotonicity, taper ratios
320 var bones_total: i64 = 0
321 var bone_sum: i64 = 0
322 var d3: i64 = 0
323 var d2: i64 = 0
324 var mono: i64 = 0
325 var rv: i64 = 0
326 var worst: i64 = 0
327 var minc: i64 = HC_BIG
328 var maxc: i64 = 0
329 var di: i64 = 0
330 while di < nd {
331 let L: i64 = DL[di]
332 if L < minc { minc = L }
333 if L > maxc { maxc = L }
334 let nb: i64 = L - 1
335 bones_total = bones_total + nb
336 if nb == HC_FINGER_BONES { d3 = d3 + 1 }
337 if nb == HC_THUMB_BONES { d2 = d2 + 1 }
338 var bi: i64 = 0
339 while bi < nb {
340 let bl: i64 = hc_dist(W, sw, DJ[di*HC_MAXCH + bi], DJ[di*HC_MAXCH + bi + 1])
341 bone_sum = bone_sum + bl
342 if bi + 1 < nb {
343 let nxt: i64 = hc_dist(W, sw, DJ[di*HC_MAXCH + bi + 1], DJ[di*HC_MAXCH + bi + 2])
344 // (1) MONOTONIC DISTAL SHORTENING -- cited, strict, no tolerance
345 if nxt >= bl { mono = mono + 1 }
346 // taper CEILING only; the floor is the clause above
347 if nxt > 0 {
348 let r: i64 = bl * HC_Q / nxt
349 if r > worst { worst = r }
350 var lim: i64 = HC_R_PMPD_MAX
351 if bi == 0 { lim = HC_R_PPPM_MAX }
352 if r > lim { rv = rv + 1 }
353 }
354 }
355 bi = bi + 1
356 }
357 di = di + 1
358 }
359 out[4] = bones_total
360 out[5] = d3
361 out[6] = d2
362 out[7] = mono
363 out[10] = rv
364 out[14] = minc
365 out[15] = maxc
366 out[17] = worst
367 var meanb: i64 = 0
368 if bones_total > 0 { meanb = bone_sum / bones_total }
369 out[12] = meanb
370 // FUSION: any two joints of DIFFERENT digits closer than HC_FUSE_PERMIL of the mean bone length
371 // are occupying one another. This is the fused-mitten class the eye reports as "bad hands".
372 let fuse: i64 = meanb * HC_FUSE_PERMIL / HC_Q
373 var fused: i64 = 0
374 var ai: i64 = 0
375 while ai < nd {
376 var bj: i64 = ai + 1
377 while bj < nd {
378 var hit: i64 = 0
379 var p: i64 = 0
380 while p < DL[ai] {
381 var q2: i64 = 0
382 while q2 < DL[bj] {
383 if hc_dist(W, sw, DJ[ai*HC_MAXCH + p], DJ[bj*HC_MAXCH + q2]) <= fuse { hit = 1 }
384 q2 = q2 + 1
385 }
386 p = p + 1
387 }
388 if hit == 1 { fused = fused + 1 }
389 bj = bj + 1
390 }
391 ai = ai + 1
392 }
393 out[8] = fused
394 // THUMB OPPOSITION: each digit's root-to-tip direction, compared pairwise by cosine in permil.
395 // The opposed digit is the one whose WORST agreement with another digit is lowest. A mitten has
396 // every digit parallel, so its opposition collapses to zero -- which is exactly the defect.
397 var opp: i64 = 0
398 if nd >= 2 {
399 var worst_pair: i64 = HC_Q
400 ai = 0
401 while ai < nd {
402 let ra: i64 = sw + 1 + DR[ai] * HC_WPJ
403 let ta: i64 = sw + 1 + DJ[ai*HC_MAXCH + DL[ai] - 1] * HC_WPJ
404 let ax2: i64 = W[ta+1] - W[ra+1]
405 let ay2: i64 = W[ta+2] - W[ra+2]
406 let az2: i64 = W[ta+3] - W[ra+3]
407 let la: i64 = hc_isqrt(ax2*ax2 + ay2*ay2 + az2*az2)
408 var bj2: i64 = ai + 1
409 while bj2 < nd {
410 let rb: i64 = sw + 1 + DR[bj2] * HC_WPJ
411 let tb: i64 = sw + 1 + DJ[bj2*HC_MAXCH + DL[bj2] - 1] * HC_WPJ
412 let bx: i64 = W[tb+1] - W[rb+1]
413 let by: i64 = W[tb+2] - W[rb+2]
414 let bz: i64 = W[tb+3] - W[rb+3]
415 let lb: i64 = hc_isqrt(bx*bx + by*by + bz*bz)
416 if la > 0 { if lb > 0 {
417 let cs: i64 = (ax2*bx + ay2*by + az2*bz) * HC_Q / (la * lb)
418 if cs < worst_pair { worst_pair = cs }
419 } }
420 bj2 = bj2 + 1
421 }
422 ai = ai + 1
423 }
424 opp = HC_Q - worst_pair
425 if opp < 0 { opp = 0 }
426 }
427 out[9] = opp
428 return 0
429}
430
431// THE PREDICATE. Every clause is a thing a hand IS. No weights, no blended number.
432func hc_ok(out: *i64) -> i64 {
433 if out[3] != HC_DIGITS_CANON { return 0 }
434 if out[5] < HC_MIN_3BONE { return 0 }
435 if out[7] > 0 { return 0 }
436 if out[8] > 0 { return 0 }
437 if out[10] > 0 { return 0 }
438 if out[9] < HC_THUMB_OPP_MIN { return 0 }
439 return 1
440}
441func hc_report(out: *i64) -> i64 {
442 hc_puts("{\x22organ\x22:\x22nx_handcheck\x22,\x22v\x22:1" as *u8)
443 hc_puts(",\x22joints\x22:" as *u8); hc_pn(out[0])
444 hc_puts(",\x22digit_hubs\x22:" as *u8); hc_pn(out[1])
445 hc_puts(",\x22hand_hubs\x22:" as *u8); hc_pn(out[16])
446 hc_puts(",\x22hub_joint\x22:" as *u8); hc_pn(out[2])
447 hc_puts(",\x22hub_h01_permil\x22:" as *u8); hc_pn(out[11])
448 hc_puts(",\x22up_axis\x22:" as *u8); hc_pn(out[13])
449 hc_puts(",\x22digits\x22:" as *u8); hc_pn(out[3])
450 hc_puts(",\x22bones_total\x22:" as *u8); hc_pn(out[4])
451 hc_puts(",\x22digits_3bone\x22:" as *u8); hc_pn(out[5])
452 hc_puts(",\x22digits_2bone\x22:" as *u8); hc_pn(out[6])
453 hc_puts(",\x22chain_joints_min\x22:" as *u8); hc_pn(out[14])
454 hc_puts(",\x22chain_joints_max\x22:" as *u8); hc_pn(out[15])
455 hc_puts(",\x22mean_bone\x22:" as *u8); hc_pn(out[12])
456 hc_puts(",\x22mono_violations\x22:" as *u8); hc_pn(out[7])
457 hc_puts(",\x22taper_violations\x22:" as *u8); hc_pn(out[10])
458 hc_puts(",\x22worst_taper_permil\x22:" as *u8); hc_pn(out[17])
459 hc_puts(",\x22fused_digit_pairs\x22:" as *u8); hc_pn(out[8])
460 hc_puts(",\x22thumb_opposition_permil\x22:" as *u8); hc_pn(out[9])
461 hc_puts(",\x22verdict\x22:\x22" as *u8)
462 if hc_ok(out) == 1 { hc_puts("GREEN" as *u8) } else { hc_puts("RED" as *u8) }
463 hc_puts("\x22,\x22canon\x22:\x22Buryanov+Kotiuk Int.J.Morphol. 28(3):755-758 2010 Tab.I and II: five rays, four with three phalanges, proximal greater than medial greater than distal in every ray\x22" as *u8)
464 hc_puts(",\x22unmeasured_in_v1\x22:\x22SKIN weights (hand-vs-forearm region), POSE and ANIM (clipping during motion), mesh-level interpenetration\x22}\n" as *u8)
465 return 0
466}
467
468// ---- fixtures: SKEL word images built in memory, so the gate needs no file and no /tmp ----------
469// layout is exactly the shipped one: W[sw]=nj then per joint [parent][x][y][z][qx][qy][qz][qw].
470func hc_setj(W: *i64, sw: i64, j: i64, p: i64, x: i64, y: i64, z: i64) -> i64 {
471 let b: i64 = sw + 1 + j * HC_WPJ
472 W[b] = p
473 W[b+1] = x
474 W[b+2] = y
475 W[b+3] = z
476 W[b+4] = 0
477 W[b+5] = 0
478 W[b+6] = 0
479 W[b+7] = HC_Q
480 return 0
481}
482// A GOOD HAND: a 300-unit-tall stub rig (root, head, wrist at 60 percent height) plus five digits.
483// Four fingers of 4 joints (3 phalanges) cut to the CITED taper 44:26:17, and a thumb of 3 joints
484// swung across the palm so it OPPOSES.
485func hc_fx_good(nout: *i64) -> *i64 {
486 let W: *i64 = sys_mmap((HC_MAXJ*HC_WPJ + 8)*8) as *i64
487 hc_setj(W, 0, 0, 0-1, 0, 0, 0) // root
488 hc_setj(W, 0, 1, 0, 0, 0, 300) // head: sets the rig extent to 300 on axis 2
489 hc_setj(W, 0, 2, 0, 100, 0, 180) // wrist, h01 = 600
490 var d: i64 = 0
491 var jn: i64 = 3
492 while d < 4 {
493 let y0: i64 = d * 20 - 30
494 hc_setj(W, 0, jn, 2, 110, y0, 180)
495 hc_setj(W, 0, jn+1, jn, 154, y0, 180)
496 hc_setj(W, 0, jn+2, jn+1, 180, y0, 180)
497 hc_setj(W, 0, jn+3, jn+2, 197, y0, 180)
498 jn = jn + 4
499 d = d + 1
500 }
501 hc_setj(W, 0, jn, 2, 104, 0-50, 180)
502 hc_setj(W, 0, jn+1, jn, 122, 0-88, 148)
503 hc_setj(W, 0, jn+2, jn+1, 134, 0-112, 128)
504 jn = jn + 3
505 W[0] = jn
506 nout[0] = jn
507 return W
508}
509// A BAD HAND -- the shape the operator reported. Same wrist, same five stubs, but each digit is ONE
510// bone, they all point the same way, and two of them share a position: a rake fused into a mitten.
511func hc_fx_bad(nout: *i64) -> *i64 {
512 let W: *i64 = sys_mmap((HC_MAXJ*HC_WPJ + 8)*8) as *i64
513 hc_setj(W, 0, 0, 0-1, 0, 0, 0)
514 hc_setj(W, 0, 1, 0, 0, 0, 300)
515 hc_setj(W, 0, 2, 0, 100, 0, 180)
516 var d: i64 = 0
517 var jn: i64 = 3
518 while d < 5 {
519 var y0: i64 = d * 20 - 40
520 if d == 4 { y0 = 20 } // digit 4 lands on digit 3 -> a fused pair
521 hc_setj(W, 0, jn, 2, 110, y0, 180)
522 hc_setj(W, 0, jn+1, jn, 180, y0, 180)
523 jn = jn + 2
524 d = d + 1
525 }
526 W[0] = jn
527 nout[0] = jn
528 return W
529}
530// A LEG, not a hand: one straight chain. Nothing on it is a hub, so the judge must ABSTAIN.
531func hc_fx_leg(nout: *i64) -> *i64 {
532 let W: *i64 = sys_mmap((HC_MAXJ*HC_WPJ + 8)*8) as *i64
533 hc_setj(W, 0, 0, 0-1, 0, 0, 300)
534 hc_setj(W, 0, 1, 0, 0, 0, 200)
535 hc_setj(W, 0, 2, 1, 0, 0, 100)
536 hc_setj(W, 0, 3, 2, 0, 0, 0)
537 W[0] = 4
538 nout[0] = 4
539 return W
540}
541// A FOOT: the SAME five-chain topology as a good hand, planted at the BOTTOM of the rig. This is the
542// control that proves the height band is doing work -- a toe hub must never be graded as a hand.
543func hc_fx_foot(nout: *i64) -> *i64 {
544 let W: *i64 = sys_mmap((HC_MAXJ*HC_WPJ + 8)*8) as *i64
545 hc_setj(W, 0, 0, 0-1, 0, 0, 300)
546 hc_setj(W, 0, 1, 0, 0, 0, 0) // ankle at the very bottom, h01 = 0
547 var d: i64 = 0
548 var jn: i64 = 2
549 while d < 5 {
550 let y0: i64 = d * 20 - 40
551 hc_setj(W, 0, jn, 1, 10, y0, 0)
552 hc_setj(W, 0, jn+1, jn, 54, y0, 0)
553 hc_setj(W, 0, jn+2, jn+1, 80, y0, 0)
554 hc_setj(W, 0, jn+3, jn+2, 97, y0, 0)
555 jn = jn + 4
556 d = d + 1
557 }
558 W[0] = jn
559 nout[0] = jn
560 return W
561}
562
563func hc_dump(W: *i64, sw: i64, nj: i64) -> i64 {
564 if nj <= 0 { hc_puts("HANDCHECK-DUMP REFUSED: SKEL carries no joints\n" as *u8); return 4 }
565 if nj > HC_MAXJ { hc_puts("HANDCHECK-DUMP REFUSED: implausible joint count\n" as *u8); return 4 }
566 let NC: *i64 = sys_mmap(HC_MAXJ*8) as *i64
567 hc_children(W, sw, nj, NC)
568 let CH: *i64 = sys_mmap(HC_MAXCH*8) as *i64
569 hc_puts("HANDCHECK-DUMP joints=" as *u8); hc_pn(nj); hc_puts("\n" as *u8)
570 var j: i64 = 0
571 while j < nj {
572 let b: i64 = sw + 1 + j * HC_WPJ
573 hc_puts("J " as *u8); hc_pn(j)
574 hc_puts(" parent=" as *u8); hc_pn(W[b])
575 hc_puts(" xyz=" as *u8); hc_pn(W[b+1]); hc_puts(" " as *u8); hc_pn(W[b+2]); hc_puts(" " as *u8); hc_pn(W[b+3])
576 hc_puts(" children=" as *u8); hc_pn(NC[j])
577 hc_puts(" chainlen=" as *u8); hc_pn(hc_chain(W, sw, nj, NC, j, CH))
578 hc_puts(" hubdigits=" as *u8); hc_pn(hc_hub_digits(W, sw, nj, NC, j, CH))
579 hc_puts("\n" as *u8)
580 j = j + 1
581 }
582 return 0
583}
584
585// THE TEETH LIVE HERE AND NOWHERE ELSE. Both entry points -- the `selftest` VERB below and the roster
586// surface nx_handcheck_gate -- pass their OWN counter into this one function, so the two can never
587// disagree about what was checked and a tooth added later cannot land in only one of them.
588func hc_teeth(ctr: *i64) -> i64 {
589 let n: *i64 = sys_mmap(16) as *i64
590 let good: *i64 = sys_mmap(HC_OUT*8) as *i64
591 let bad: *i64 = sys_mmap(HC_OUT*8) as *i64
592 let leg: *i64 = sys_mmap(HC_OUT*8) as *i64
593 let foot: *i64 = sys_mmap(HC_OUT*8) as *i64
594 let Wg: *i64 = hc_fx_good(n)
595 let njg: i64 = n[0]
596 let rg: i64 = hc_measure(Wg, 0, njg, good)
597 let Wb: *i64 = hc_fx_bad(n)
598 let njb: i64 = n[0]
599 let rb: i64 = hc_measure(Wb, 0, njb, bad)
600 let Wl: *i64 = hc_fx_leg(n)
601 let rl: i64 = hc_measure(Wl, 0, n[0], leg)
602 let Wf: *i64 = hc_fx_foot(n)
603 let rf: i64 = hc_measure(Wf, 0, n[0], foot)
604
605 // T1 FIXTURE REACHED THE CONDITION. Assert the topology walk actually ran before asserting any
606 // outcome -- a fixture the measurement never examined proves nothing about the measurement.
607 var t1: i64 = 0
608 if rg == 0 { if njg == 22 { if good[0] == 22 { t1 = 1 } } }
609 gv_check("T1 fixture-reached-the-condition: the good hand parses as 22 joints and MEASURES" as *u8, t1, ctr)
610
611 // T2 the hand is FOUND STRUCTURALLY -- five terminal chains on one attachment, no names, no index
612 var t2: i64 = 0
613 if good[3] == HC_DIGITS_CANON { if good[2] == 2 { t2 = 1 } }
614 gv_check("T2 finds the hand by TOPOLOGY: 5 digit chains on joint 2, with no joint names anywhere" as *u8, t2, ctr)
615
616 // T3 the CITED phalanx count: four fingers of three bones, one thumb of two
617 var t3: i64 = 0
618 if good[5] == 4 { if good[6] == 1 { if good[4] == 14 { t3 = 1 } } }
619 gv_check("T3 phalanx count matches the cited canon: 4 digits x 3 bones plus 1 thumb x 2 = 14" as *u8, t3, ctr)
620
621 // T4 MONOTONIC DISTAL SHORTENING is measured, and it is clean on a hand cut to the cited table
622 var t4: i64 = 0
623 if good[7] == 0 { if good[12] > 0 { t4 = 1 } }
624 gv_check("T4 monotonic distal shortening: zero violations on a hand cut to the cited 44:26:17 taper" as *u8, t4, ctr)
625
626 // T5 DISCRIMINATION, which is the whole job of a judge. The rake is one bone per digit.
627 var t5: i64 = 0
628 if rb == 0 { if bad[5] == 0 { if bad[4] == 5 { t5 = 1 } } }
629 gv_check("T5 DISCRIMINATES: the rake reports 5 bones total and ZERO three-boned digits" as *u8, t5, ctr)
630
631 // T6 the fused pair is COUNTED, not merely flagged -- one pair planted, one pair found
632 var t6: i64 = 0
633 if bad[8] == 1 { if good[8] == 0 { t6 = 1 } }
634 gv_check("T6 fusion is COUNTED: exactly the one planted overlapping digit pair, none on the good hand" as *u8, t6, ctr)
635
636 // T7 thumb opposition separates a hand from a rake by construction
637 var t7: i64 = 0
638 if bad[9] < HC_THUMB_OPP_MIN { if good[9] >= HC_THUMB_OPP_MIN { t7 = 1 } }
639 gv_check("T7 opposition: the all-parallel rake falls under the floor, the opposed thumb clears it" as *u8, t7, ctr)
640
641 // T8 ANTI-VACUITY. A judge that passes everything is not a judge.
642 var t8: i64 = 0
643 if hc_ok(good) == 1 { if hc_ok(bad) == 0 { t8 = 1 } }
644 gv_check("T8 anti-vacuity: verdict GREEN on the built hand, RED on the rake" as *u8, t8, ctr)
645
646 // T9 ABSTAIN, NEVER ACQUIT. A rig with no digit chain must be REFUSED with its own return code,
647 // and its verdict must not be readable as a pass.
648 var t9: i64 = 0
649 if rl == 0 - 2 { if hc_ok(leg) == 0 { if leg[1] == 0 { t9 = 1 } } }
650 gv_check("T9 neg-control-abstain-on-no-hand: a chain with no hub returns -2 and never reads GREEN" as *u8, t9, ctr)
651
652 // T10 the SAME five-chain topology at the bottom of the rig must be refused by the height band.
653 // Without this the judge would grade a toe cluster as a hand and report a clean pass about a
654 // part it never looked at.
655 var t10: i64 = 0
656 if rf == 0 - 3 { if foot[1] > 0 { if foot[16] == 0 { t10 = 1 } } }
657 gv_check("T10 neg-control-foot-is-not-a-hand: an identical hub at h01 zero is FOUND but NOT graded" as *u8, t10, ctr)
658
659 // T11 the taper CEILING fires only above its bound, and the good hand sits under it
660 var t11: i64 = 0
661 if good[10] == 0 { if good[17] > 0 { if good[17] < HC_R_PPPM_MAX { t11 = 1 } } }
662 gv_check("T11 taper ceiling: the cited hand measures a real worst taper and stays under the bound" as *u8, t11, ctr)
663
664 // T12 the ruler is not vacuous on the empty set: zero joints must refuse, not report a clean hand
665 let emp: *i64 = sys_mmap(HC_OUT*8) as *i64
666 let We: *i64 = sys_mmap(64*8) as *i64
667 We[0] = 0
668 let re: i64 = hc_measure(We, 0, 0, emp)
669 var t12: i64 = 0
670 if re == 0 - 1 { if hc_ok(emp) == 0 { t12 = 1 } }
671 gv_check("T12 neg-control-empty-skel: zero joints is REFUSED -- a tooth that passes on the empty set is not a tooth" as *u8, t12, ctr)
672
673 return 0
674}
675// The VERB entry point. nx_handcheck_gate is the ROSTER entry point and builds its OWN counter over
676// these SAME teeth -- see that organ header for why a roster row must never be this judge forked bare.
677func hc_gate() -> i64 {
678 let ctr: *i64 = gv_ctr()
679 gv_head("nx_handcheck selftest -- the hand structure judge, proven to SEPARATE a rake from a hand" as *u8)
680 hc_teeth(ctr)
681 return gv_verdict("HANDCHECK-GATE" as *u8, ctr, "hand structure judge: topological digit discovery, cited phalanx count and monotonic taper, fusion, opposition, abstain on no-hand and on a foot" as *u8)
682}
683
684
685func hc_load(path: *u8, lp: *i64, swp: *i64, njp: *i64) -> *u8 {
686 let b: *u8 = sys_read_file(path, lp)
687 if (b as i64) == 0 { swp[0] = 0 - 9; return 0 as *u8 }
688 let sw: i64 = nxa_find(b, lp[0], nxa_tag4("SKEL" as *u8))
689 if sw < 0 { swp[0] = sw; return 0 as *u8 }
690 let W: *i64 = b as *i64
691 swp[0] = sw
692 njp[0] = W[sw]
693 return b
694}
695
696func main(argc: i64, argv: *i64) -> i64 {
697 if argc >= 2 {
698 if hc_streq(argv[1] as *u8, "selftest" as *u8) == 1 { return hc_gate() }
699 var mode: i64 = 0
700 if hc_streq(argv[1] as *u8, "check" as *u8) == 1 { mode = 1 }
701 if hc_streq(argv[1] as *u8, "dump" as *u8) == 1 { mode = 2 }
702 if mode > 0 {
703 if argc < 3 { hc_puts("{\x22error\x22:\x22usage: nx_handcheck check <file.nxa> | dump <file.nxa> | selftest\x22}\n" as *u8); return 2 }
704 let lp: *i64 = sys_mmap(16) as *i64
705 let swp: *i64 = sys_mmap(16) as *i64
706 let njp: *i64 = sys_mmap(16) as *i64
707 swp[0] = 0
708 njp[0] = 0
709 let b: *u8 = hc_load(argv[2] as *u8, lp, swp, njp)
710 if (b as i64) == 0 {
711 hc_puts("{\x22organ\x22:\x22nx_handcheck\x22,\x22verdict\x22:\x22REFUSED\x22,\x22why\x22:\x22cannot read the asset or it carries no valid SKEL section\x22,\x22skel_rc\x22:" as *u8)
712 hc_pn(swp[0]); hc_puts("}\n" as *u8)
713 return 3
714 }
715 let W: *i64 = b as *i64
716 if mode == 2 { return hc_dump(W, swp[0], njp[0]) }
717 let out: *i64 = sys_mmap(HC_OUT*8) as *i64
718 let rc: i64 = hc_measure(W, swp[0], njp[0], out)
719 if rc < 0 {
720 hc_puts("{\x22organ\x22:\x22nx_handcheck\x22,\x22verdict\x22:\x22UNOBSERVABLE-no-hand-chain\x22,\x22rc\x22:" as *u8); hc_pn(rc)
721 hc_puts(",\x22joints\x22:" as *u8); hc_pn(out[0])
722 hc_puts(",\x22digit_hubs\x22:" as *u8); hc_pn(out[1])
723 hc_puts(",\x22hand_hubs\x22:" as *u8); hc_pn(out[16])
724 hc_puts(",\x22why\x22:\x22no attachment on this rig hosts three or more terminal digit chains above the hip band -- the judge reports NOTHING rather than a hand it never measured\x22}\n" as *u8)
725 return 4
726 }
727 hc_report(out)
728 if hc_ok(out) == 1 { return 0 }
729 return 1
730 }
731 }
732 hc_puts("{\x22organ\x22:\x22nx_handcheck\x22,\x22usage\x22:\x22nx_handcheck check <file.nxa> | nx_handcheck dump <file.nxa> | nx_handcheck selftest\x22}\n" as *u8)
733 return 0
734}