code wiki / (root) / nx_handcheck.nx

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}