nx_toolreg_reconcile.nx source
↩ module page · 125 lines · 9311 B
1// nx_toolreg_reconcile.nx -- CLI half of the discovery reconciler / drift auditor. Run from nishihost CWD:
2// nx_toolreg_reconcile [allowlist] [schemas] reconcile (register missing rows); default paths
3// nx_toolreg_reconcile reconcile [allowlist] [schemas] same, with the verb stated (2026-08-16)
4// nx_toolreg_reconcile check [allowlist] [schemas] AUDIT: register NOTHING, exit 1 if drift>0
5// nx_toolreg_reconcile heal [allowlist] [schemas] SELF-MANAGING: auto-register everything that HAS
6// a schema, then RED only on the no-schema residue a human must author. This is the verb the
7// standing sweep runs, and it was missing from this banner while `reconcile` -- which the parser
8// did not even accept -- was listed. THE MOST IMPORTANT VERB WAS THE ONE NOT DOCUMENTED.
9// Reconcile registers every GREEN allowlisted tool missing from discovery using the author's own schema
10// row; skips (reports) tools with no schema row. Idempotent. `check` is the read-only regression tooth:
11// it exits non-zero if ANY GREEN tool is unregistered or schema-less -- run it in the standing sweep so
12// the discovery-complete state (allowlist == tools/list) can never silently regrow.
13// exit: 0 clean | 1 io / failed rows / (check) drift>0 license_tier: ORIGINAL
14import "nx_toolreg_reconcile_lib.nx"
15
16const RC_ARG_ALLOW: i64 = 1 // argv: optional allowlist path (reconcile mode)
17const RC_ARG_SCH: i64 = 2 // argv: optional schemas path (reconcile mode)
18const RC_ARGC_A: i64 = 2 // argc with allowlist
19const RC_ARGC_S: i64 = 3 // argc with both
20const RC_CK_ARG_ALLOW: i64 = 2 // argv: optional allowlist path (check mode, after the verb)
21const RC_CK_ARG_SCH: i64 = 3 // argv: optional schemas path (check mode)
22const RC_CK_ARGC_A: i64 = 3 // argc: check + allowlist
23const RC_CK_ARGC_S: i64 = 4 // argc: check + allowlist + schemas
24const RC_ASCII_0: i64 = 48 // '0'
25const RC_DEC: i64 = 10 // decimal base
26
27func rc_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
28func rc_putn(v: i64) -> i64 {
29 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
30 var m: i64 = v; if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
31 let d: *u8 = sys_mmap(32); var k: i64 = 0
32 while m > 0 { d[k] = (RC_ASCII_0 + (m % RC_DEC)) as u8; m = m / RC_DEC; k = k + 1 }
33 let o: *u8 = sys_mmap(32); var i: i64 = 0
34 while i < k { o[i] = d[k-1-i]; i = i + 1 } sys_write(1, o, k)
35 return 0
36}
37
38func rc_seq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 }
39
40func main(argc: i64, argv: *i64) -> i64 {
41 var allow: *u8 = "tool_allowlist.conf" as *u8
42 var sch: *u8 = "knowledge/tool_schemas.conf" as *u8
43 let counts: *i64 = sys_mmap(RR_COUNTS) as *i64
44 // AUDIT MODE: `check [allow] [schemas]` -- register nothing, exit 1 if drift>0 (the regression tooth)
45 if argc >= RC_ARGC_A { if rc_seq(argv[RC_ARG_ALLOW] as *u8, "check" as *u8) == 1 {
46 if argc >= RC_CK_ARGC_A { allow = argv[RC_CK_ARG_ALLOW] as *u8 }
47 if argc >= RC_CK_ARGC_S { sch = argv[RC_CK_ARG_SCH] as *u8 }
48 let drift: i64 = rr_drift(TOOL_PREFIX, allow, sch, counts)
49 if drift < 0 { rc_puts("NX-TOOLREG-AUDIT IO: cannot read allowlist/schemas\n" as *u8); return 1 }
50 // Called BEFORE the audit line is emitted: rr_missing_rows PRINTS its per-row worklist, and a
51 // worklist flushed mid-line would corrupt the one line every downstream parser anchors on.
52 let missing: i64 = rr_missing_rows(allow, sch, 1)
53 rc_puts("NX-TOOLREG-AUDIT drift=" as *u8); rc_putn(drift)
54 rc_puts(" (would-register=" as *u8); rc_putn(counts[RR_C_REG])
55 rc_puts(" no-schema=" as *u8); rc_putn(counts[RR_C_NOSCHEMA])
56 // Scaffold is REPORTED but not counted as drift: a row pointing at `<t>.elf.new` is staged,
57 // never promoted, and cannot be "finished" by writing it a contract. Keeping it in drift made
58 // this gate permanently RED for 14 rows that were never going to move.
59 rc_puts(" scaffold=" as *u8); rc_putn(counts[RR_C_SCAFFOLD])
60 rc_puts(" no-authored-row=" as *u8); rc_putn(counts[RR_C_NOAUTH])
61 rc_puts(" dup-schema=" as *u8); rc_putn(counts[RR_C_DUPSCHEMA])
62 // STALE-DESC printed 2026-08-14. It was counted internally (RR_C_UPDATED) and printed NOWHERE in
63 // audit mode, so a row whose description had drifted left EVERY visible bucket at zero: MEASURED
64 // on a one-row fixture, rows=1 and all six printed buckets read 0, including already-discoverable.
65 // A PARTITION THAT DOES NOT SUM IS A LEAK, AND THE LEAKED ROW WAS THE ONLY DEFECT PRESENT.
66 rc_puts(" stale-desc=" as *u8); rc_putn(counts[RR_C_UPDATED])
67 rc_puts(" already-discoverable=" as *u8); rc_putn(counts[RR_C_ALREADY])
68 // THE REVERSE JOIN, DECLARED A SEPARATE AXIS (2026-08-20). Every bucket above is a fact about a
69 // row that IS in the allowlist; this is the only one that can see a row that ISN'T. It does NOT
70 // vote in drift (rr_missing_rows says why the overlap forbids folding it in), and -1 means the
71 // census could not read its inputs -- UNMEASURED, never zero.
72 rc_puts(" missing-row=" as *u8); rc_putn(missing)
73 rc_puts(") verdict=" as *u8)
74 if drift == 0 { rc_puts("GREEN (allowlist == tools/list; every tool contracted)\n" as *u8); return 0 }
75 rc_puts("RED (run reconcile: it registers missing rows AND rewrites stale descriptions / add missing tool_schemas.conf rows)\n" as *u8)
76 return 1
77 } }
78 // HEAL MODE: `heal [allow] [schemas]` -- SELF-MANAGING. Auto-register all has-schema tools (the
79 // mechanical drift that needs no human), then report ONLY the genuine no-schema residue (named). Exit 0
80 // when the residue is empty (discovery self-healed), exit 1 when tools are allowlisted with NO contract
81 // (real AUTHOR debt a human must fix). This is what the standing sweep runs: it FIXES what it can and
82 // reds only on the un-authorable -- retiring the manual-reconcile treadmill.
83 if argc >= RC_ARGC_A { if rc_seq(argv[RC_ARG_ALLOW] as *u8, "heal" as *u8) == 1 {
84 if argc >= RC_CK_ARGC_A { allow = argv[RC_CK_ARG_ALLOW] as *u8 }
85 if argc >= RC_CK_ARGC_S { sch = argv[RC_CK_ARG_SCH] as *u8 }
86 if rr_reconcile(TOOL_PREFIX, allow, sch, counts) < 0 { rc_puts("NX-TOOLREG-HEAL IO: cannot read allowlist/schemas\n" as *u8); return 1 }
87 let names: *u8 = sys_mmap(RR_FILE_CAP)
88 let ns: i64 = rr_no_schema_names(TOOL_PREFIX, allow, sch, names, RR_FILE_CAP)
89 rc_puts("NX-TOOLREG-HEAL auto-registered=" as *u8); rc_putn(counts[RR_C_REG])
90 rc_puts(" already=" as *u8); rc_putn(counts[RR_C_ALREADY])
91 rc_puts(" no-schema-residue=" as *u8); rc_putn(ns)
92 rc_puts(" verdict=" as *u8)
93 if ns == 0 { rc_puts("GREEN (discovery self-healed; every tool contracted)\n" as *u8); return 0 }
94 rc_puts("RED needs-author-schema: " as *u8); rc_puts(names); rc_puts("\n" as *u8)
95 return 1
96 } }
97 // THE DOCUMENTED TOKEN THE PARSER NEVER ACCEPTED. The banner at the top of this file shows
98 // `reconcile` as a verb, but only `check` and `heal` had cases -- so typing the word the banner
99 // prints made it the ALLOWLIST PATH, and the run died with "cannot read allowlist/schemas", a
100 // message about the wrong subject entirely. MEASURED 2026-08-16: that is exactly what happened, and
101 // finding the cause took a source read.
102 // ★A USAGE LINE THAT SHOWS A TOKEN THE PARSER HAS NO CASE FOR IS A COMMENT ASSERTING A CONTRACT THE
103 // CODE DOES NOT HONOUR -- and the reader trusts the banner, because that is what a banner is for.
104 // ADDITIVE ON PURPOSE: the no-arg default is UNCHANGED, so every existing call site keeps working.
105 // This organ has NINE call-site candidates (nx_wiredclaim_gate, same day), which is precisely why
106 // the fix teaches the parser the documented word rather than changing what no arguments means.
107 var vbase: i64 = 0
108 if argc >= RC_ARGC_A { if rc_seq(argv[RC_ARG_ALLOW] as *u8, "reconcile" as *u8) == 1 { vbase = 1 } }
109 // RECONCILE MODE (default): register missing rows
110 if argc >= RC_ARGC_A + vbase { allow = argv[RC_ARG_ALLOW + vbase] as *u8 }
111 if argc >= RC_ARGC_S + vbase { sch = argv[RC_ARG_SCH + vbase] as *u8 }
112 let rows: i64 = rr_reconcile(TOOL_PREFIX, allow, sch, counts)
113 if rows < 0 { rc_puts("NX-TOOLREG-RECONCILE IO: cannot read allowlist/schemas\n" as *u8); return 1 }
114 rc_puts("NX-TOOLREG-RECONCILE rows=" as *u8); rc_putn(rows)
115 rc_puts(" registered=" as *u8); rc_putn(counts[RR_C_REG])
116 rc_puts(" already=" as *u8); rc_putn(counts[RR_C_ALREADY])
117 // UPDATED is reported separately from REGISTERED so a description CORRECTION is visible as
118 // work done, not hidden inside 'already' (debt seq1526). 0 on a no-change run = idempotence.
119 rc_puts(" updated=" as *u8); rc_putn(counts[RR_C_UPDATED])
120 rc_puts(" skipped-no-schema=" as *u8); rc_putn(counts[RR_C_NOSCHEMA])
121 rc_puts(" failed=" as *u8); rc_putn(counts[RR_C_FAIL])
122 rc_puts("\n" as *u8)
123 if counts[RR_C_FAIL] > 0 { return 1 }
124 return 0
125}