nx_project.nx source
↩ module page · 3026 lines · 128039 B
1// nx_project.nx -- NISHI PROJECT: the team + contractor project layer for nishifamily.com/project.
2// Operator 2026-07-09: "get beyond microsoft project and leantime ... work with my team and contractors".
3// COMPOSES the existing substrate rather than re-implementing it: tasks/deps/completions live in the
4// nx_vizsla_plan record plane (tsk:/don: records; plan field == project id). This organ adds the layer
5// the projectmgmt census measured ABSENT:
6// prj: PROJECT meta (pid, owner, title)
7// mem: MEMBERSHIP with roles (owner|team|contractor) + data-driven capacity (cap = minutes, or -)
8// fail-closed ROLE-SCOPED VIEW: owner/team see all tasks; contractor sees ONLY own tasks; a
9// non-member is DENIED (deny by default -- access is granted by a record, never assumed)
10// WORKLOAD rollup: per-member open effort vs recorded capacity (over-flag only when cap is data)
11// Task state derives from the plan records: DONE (don: exists) / BLOCKED (a dep lacks don:) / READY.
12// Records (load <file> <prefix> [segid], additive + CID-idempotent):
13// PROJ <pid> <owner> <title>
14// MEMBER <pid> <person> <role> <cap|->
15// Commands (pj_cli; built via the _hdl_build wrapper; ARGLESS = selftest gate incl a fork of the
16// REAL blessed nx_vizsla_plan.elf as the independent tsk:/don: writer -- cross-organ compose proof):
17// load <file> <prefix> [segid] | view <prefix> <pid> <person> | workload <prefix> <pid>
18// expect_exit: 0 license_tier: ORIGINAL
19import "nx_syscalls.nx"
20import "nx_canon_cid.nx"
21import "nx_seg_store.nx"
22const K_MAGIC_1970: i64 = 1970
23const K_MAGIC_146097: i64 = 146097
24const K_MAGIC_719468: i64 = 719468
25const K_MAGIC_2048: i64 = 2048
26const K_MAGIC_1024: i64 = 1024
27const K_MAGIC_65536: i64 = 65536
28const K_MAGIC_146096: i64 = 146096
29const K_MAGIC_1460: i64 = 1460
30const K_MAGIC_36524: i64 = 36524
31
32func pj_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
33func pj_p(s: *u8) -> i64 { sys_write(1, s, pj_slen(s)); return 0 }
34
35func pj_eq(a: *u8, b: *u8) -> i64 {
36 var i: i64 = 0
37 var go: i64 = 1
38 while go == 1 {
39 if a[i] != b[i] { return 0 }
40 if a[i] == (0 as u8) { return 1 }
41 i = i + 1
42 }
43 return 0
44}
45
46func pj_cmpgt(a: *u8, b: *u8) -> i64 {
47 var i: i64 = 0
48 var go: i64 = 1
49 while go == 1 {
50 let ca: i64 = a[i] as i64
51 let cb: i64 = b[i] as i64
52 if ca > cb { return 1 }
53 if ca < cb { return 0 }
54 if ca == 0 { return 0 }
55 i = i + 1
56 }
57 return 0
58}
59
60func pj_dup(s: *u8) -> *u8 {
61 let n: i64 = pj_slen(s)
62 let d: *u8 = sys_mmap(n + 2)
63 var i: i64 = 0
64 while i <= n { d[i] = s[i]; i = i + 1 }
65 return d
66}
67
68func pj_atoi(s: *u8) -> i64 {
69 var v: i64 = 0
70 var i: i64 = 0
71 while s[i] != (0 as u8) {
72 let d: i64 = (s[i] as i64) - 48
73 if d >= 0 { if d <= 9 { v = v * 10 + d } }
74 i = i + 1
75 }
76 return v
77}
78
79func pj_digits(s: *u8) -> i64 {
80 if s[0] == (0 as u8) { return 0 }
81 var i: i64 = 0
82 while s[i] != (0 as u8) {
83 let d: i64 = (s[i] as i64) - 48
84 if d < 0 { return 0 }
85 if d > 9 { return 0 }
86 i = i + 1
87 }
88 return 1
89}
90
91func pj_isws(c: i64) -> i64 {
92 if c == 32 { return 1 }
93 if c == 13 { return 1 }
94 if c == 9 { return 1 }
95 return 0
96}
97
98func pj_tok(b: *u8, off: i64, lend: i64, dst: *u8, cap: i64) -> i64 {
99 var p: i64 = off
100 var go: i64 = 1
101 while go == 1 {
102 if p >= lend { go = 0 } else {
103 if pj_isws(b[p] as i64) == 1 { p = p + 1 } else { go = 0 }
104 }
105 }
106 var t: i64 = 0
107 go = 1
108 while go == 1 {
109 if p >= lend { go = 0 } else {
110 if pj_isws(b[p] as i64) == 1 { go = 0 } else {
111 if t < cap - 1 { dst[t] = b[p]; t = t + 1 }
112 p = p + 1
113 }
114 }
115 }
116 dst[t] = 0 as u8
117 return p
118}
119
120func pj_cat(dst: *u8, off: i64, s: *u8) -> i64 {
121 var i: i64 = 0
122 while s[i] != (0 as u8) { dst[off + i] = s[i]; i = i + 1 }
123 return off + i
124}
125
126func pj_catn(dst: *u8, off: i64, v: i64) -> i64 {
127 var o: i64 = off
128 var m: i64 = v
129 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m }
130 let t: *u8 = sys_mmap(28)
131 var k: i64 = 0
132 if m == 0 { t[0] = 48 as u8; k = 1 }
133 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
134 var i: i64 = 0
135 while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 }
136 return o + k
137}
138
139func pj_kmemeq(b: *u8, off: i64, n: i64, s: *u8) -> i64 {
140 if pj_slen(s) != n { return 0 }
141 var i: i64 = 0
142 while i < n {
143 if b[off + i] != s[i] { return 0 }
144 i = i + 1
145 }
146 return 1
147}
148
149func pj_field(rec: *u8, rl: i64, want: *u8, out: *u8, cap: i64) -> i64 {
150 if rl < 8 { return 0 }
151 let nf: i64 = ss_r32(rec, 4)
152 var off: i64 = 8
153 var fi: i64 = 0
154 while fi < nf {
155 if off + 8 > rl { return 0 }
156 let kl: i64 = ss_r32(rec, off)
157 let koff: i64 = off + 4
158 let vl: i64 = ss_r32(rec, koff + kl)
159 let voff: i64 = koff + kl + 4
160 if pj_kmemeq(rec, koff, kl, want) == 1 {
161 var t: i64 = 0
162 while t < vl {
163 if t < cap - 1 { out[t] = rec[voff + t] }
164 t = t + 1
165 }
166 if t > cap - 1 { t = cap - 1 }
167 out[t] = 0 as u8
168 return 1
169 }
170 off = voff + vl
171 fi = fi + 1
172 }
173 return 0
174}
175
176func pj_iskind(b: *u8, koff: i64, kl: i64, pfx: *u8) -> i64 {
177 if kl <= 4 { return 0 }
178 return pj_kmemeq(b, koff, 4, pfx)
179}
180
181func pj_find(list: *i64, n: i64, s: *u8) -> i64 {
182 var i: i64 = 0
183 while i < n {
184 if pj_eq(list[i] as *u8, s) == 1 { return i }
185 i = i + 1
186 }
187 return 0 - 1
188}
189
190// civil date "YYYY-MM-DD" -> days since epoch; -1 on malformed (mirrors the proven planner date math)
191func pj_datedays(s: *u8) -> i64 {
192 let nums: *i64 = sys_mmap(8 * 4) as *i64
193 var nn: i64 = 0
194 var cur: i64 = 0
195 var indig: i64 = 0
196 var i: i64 = 0
197 var go: i64 = 1
198 while go == 1 {
199 let c: i64 = s[i] as i64
200 var isd: i64 = 0
201 if c >= 48 { if c <= 57 { isd = 1 } }
202 if isd == 1 { cur = cur * 10 + (c - 48); indig = 1 }
203 if isd == 0 {
204 if indig == 1 {
205 if nn < 4 { nums[nn] = cur; nn = nn + 1 }
206 cur = 0
207 indig = 0
208 }
209 }
210 if c == 0 { go = 0 }
211 i = i + 1
212 }
213 if nn != 3 { return 0 - 1 }
214 var y: i64 = nums[0]
215 let m: i64 = nums[1]
216 let d: i64 = nums[2]
217 if y < K_MAGIC_1970 { return 0 - 1 }
218 if m < 1 { return 0 - 1 }
219 if m > 12 { return 0 - 1 }
220 if d < 1 { return 0 - 1 }
221 if d > 31 { return 0 - 1 }
222 if m <= 2 { y = y - 1 }
223 let era: i64 = y / 400
224 let yoe: i64 = y - era * 400
225 var mp: i64 = m - 3
226 if m <= 2 { mp = m + 9 }
227 let doy: i64 = (153 * mp + 2) / 5 + d - 1
228 let doe: i64 = yoe * 365 + yoe / 4 - yoe / 100 + doy
229 return era * K_MAGIC_146097 + doe - K_MAGIC_719468
230}
231
232func pj_contains(b: *u8, sz: i64, s: *u8) -> i64 {
233 let n: i64 = pj_slen(s)
234 if n == 0 { return 1 }
235 var i: i64 = 0
236 while i + n <= sz {
237 var k: i64 = 0
238 var hit: i64 = 1
239 while k < n {
240 if b[i + k] != s[k] { hit = 0; k = n } else { k = k + 1 }
241 }
242 if hit == 1 { return 1 }
243 i = i + 1
244 }
245 return 0
246}
247
248func pj_wfile(path: *u8, s: *u8) -> i64 {
249 let fd: i64 = sys_openat_wr(path, 0x1a4)
250 if fd < 0 { return 1 }
251 sys_write(fd, s, pj_slen(s))
252 sys_close(fd)
253 return 0
254}
255
256func pj_starts(s: *u8, pfx: *u8) -> i64 {
257 var i: i64 = 0
258 while pfx[i] != (0 as u8) {
259 if s[i] != pfx[i] { return 0 }
260 i = i + 1
261 }
262 return 1
263}
264
265// boundary guard for MCP callers: store paths live under knowledge/project/ or /tmp/, no dot-dot
266func pj_pathok(s: *u8) -> i64 {
267 var i: i64 = 0
268 while s[i] != (0 as u8) {
269 if s[i] == (46 as u8) { if s[i + 1] == (46 as u8) { return 0 } }
270 i = i + 1
271 }
272 if pj_starts(s, "knowledge/project/" as *u8) == 1 { return 1 }
273 if pj_starts(s, "/tmp/" as *u8) == 1 { return 1 }
274 return 0
275}
276
277func pj_pathfail() -> i64 {
278 pj_p("PROJECT path must live under knowledge/project/ or /tmp/ with no dot-dot -- fail loud\n" as *u8)
279 return 1
280}
281
282// content-addressed additive put (mirrors the proven planner writer; CID-idempotent by construction)
283func pj_put(keys: *i64, vals: *i64, nf: i64, kp: *u8, st: *i64) -> i64 {
284 let enc: *u8 = sys_mmap(K_MAGIC_2048)
285 let el: i64 = canon_encode(keys, vals, nf, enc)
286 let cid: *u8 = sys_mmap(96)
287 cid_of(enc, el, cid)
288 let kbuf: *u8 = sys_mmap(128)
289 var ko: i64 = 0
290 ko = pj_cat(kbuf, ko, kp)
291 ko = pj_cat(kbuf, ko, cid)
292 kbuf[ko] = 0 as u8
293 let seen: *i64 = st[1] as *i64
294 var fresh: i64 = 1
295 if pj_find(seen, st[2], kbuf) >= 0 { fresh = 0; st[4] = st[4] + 1 }
296 if fresh == 1 {
297 if st[2] < K_MAGIC_2048 {
298 let dk: *u8 = pj_dup(kbuf)
299 seen[st[2]] = dk as i64
300 st[2] = st[2] + 1
301 }
302 }
303 if fresh == 1 {
304 let pp: *i64 = sys_mmap(16) as *i64
305 let ll: *i64 = sys_mmap(16) as *i64
306 if ss_get_idx(st[6] as *u8, kbuf, pp, ll) == 1 { fresh = 0; st[5] = st[5] + 1 }
307 }
308 if fresh == 1 {
309 if ss_add(st[0] as *i64, 1, kbuf, enc, el) != 0 { return 0 - 1 }
310 st[3] = st[3] + 1
311 }
312 return 0
313}
314
315// ---------------- LOAD (PROJ / MEMBER lines -> prj: / mem: records) ----------------
316func pj_cmd_load(argc: i64, argv: *i64) -> i64 {
317 if argc < 4 { pj_p("PROJECT load needs <file> <prefix> -- fail loud\n" as *u8); return 1 }
318 let efile: *u8 = argv[2] as *u8
319 let prefix: *u8 = argv[3] as *u8
320 if pj_pathok(efile) == 0 { return pj_pathfail() }
321 if pj_pathok(prefix) == 0 { return pj_pathfail() }
322 var segid: i64 = 0
323 if argc > 4 { segid = pj_atoi(argv[4] as *u8) }
324 if segid == 0 { segid = sys_now_us() }
325
326 let szp: *i64 = sys_mmap(16) as *i64
327 let b: *u8 = ss_readall(efile, szp)
328 let sz: i64 = szp[0]
329 if sz <= 0 { pj_p("PROJECT file MISSING/EMPTY -- fail loud\n" as *u8); return 1 }
330
331 let pkeys: *i64 = sys_mmap(8 * 8) as *i64
332 let pvals: *i64 = sys_mmap(8 * 8) as *i64
333 pkeys[0] = "kind" as *u8 as i64
334 pkeys[1] = "pid" as *u8 as i64
335 pkeys[2] = "owner" as *u8 as i64
336 pkeys[3] = "title" as *u8 as i64
337 pvals[0] = "prj" as *u8 as i64
338 let mkeys: *i64 = sys_mmap(8 * 8) as *i64
339 let mvals: *i64 = sys_mmap(8 * 8) as *i64
340 mkeys[0] = "kind" as *u8 as i64
341 mkeys[1] = "pid" as *u8 as i64
342 mkeys[2] = "person" as *u8 as i64
343 mkeys[3] = "role" as *u8 as i64
344 mkeys[4] = "cap" as *u8 as i64
345 mvals[0] = "mem" as *u8 as i64
346 let bkeys: *i64 = sys_mmap(8 * 8) as *i64
347 let bvals: *i64 = sys_mmap(8 * 8) as *i64
348 bkeys[0] = "kind" as *u8 as i64
349 bkeys[1] = "pid" as *u8 as i64
350 bkeys[2] = "amount" as *u8 as i64
351 bvals[0] = "bud" as *u8 as i64
352 let ckeys: *i64 = sys_mmap(8 * 8) as *i64
353 let cvals: *i64 = sys_mmap(8 * 8) as *i64
354 ckeys[0] = "kind" as *u8 as i64
355 ckeys[1] = "pid" as *u8 as i64
356 ckeys[2] = "tid" as *u8 as i64
357 ckeys[3] = "amount" as *u8 as i64
358 ckeys[4] = "date" as *u8 as i64
359 cvals[0] = "cst" as *u8 as i64
360 let wkeys: *i64 = sys_mmap(8 * 8) as *i64
361 let wvals: *i64 = sys_mmap(8 * 8) as *i64
362 wkeys[0] = "kind" as *u8 as i64
363 wkeys[1] = "pid" as *u8 as i64
364 wkeys[2] = "mpd" as *u8 as i64
365 wkeys[3] = "tight" as *u8 as i64
366 wvals[0] = "wcf" as *u8 as i64
367 let lkeys: *i64 = sys_mmap(8 * 8) as *i64
368 let lvals: *i64 = sys_mmap(8 * 8) as *i64
369 lkeys[0] = "kind" as *u8 as i64
370 lkeys[1] = "pid" as *u8 as i64
371 lkeys[2] = "tid" as *u8 as i64
372 lkeys[3] = "person" as *u8 as i64
373 lkeys[4] = "date" as *u8 as i64
374 lkeys[5] = "min" as *u8 as i64
375 lvals[0] = "log" as *u8 as i64
376 let nkeys: *i64 = sys_mmap(8 * 8) as *i64
377 let nvals: *i64 = sys_mmap(8 * 8) as *i64
378 nkeys[0] = "kind" as *u8 as i64
379 nkeys[1] = "pid" as *u8 as i64
380 nkeys[2] = "tid" as *u8 as i64
381 nkeys[3] = "person" as *u8 as i64
382 nkeys[4] = "date" as *u8 as i64
383 nkeys[5] = "text" as *u8 as i64
384 nvals[0] = "not" as *u8 as i64
385 let akeys: *i64 = sys_mmap(8 * 8) as *i64
386 let avals: *i64 = sys_mmap(8 * 8) as *i64
387 akeys[0] = "kind" as *u8 as i64
388 akeys[1] = "pid" as *u8 as i64
389 akeys[2] = "tid" as *u8 as i64
390 akeys[3] = "person" as *u8 as i64
391 akeys[4] = "date" as *u8 as i64
392 avals[0] = "asg" as *u8 as i64
393 let bkeys2: *i64 = sys_mmap(8 * 8) as *i64
394 let bvals2: *i64 = sys_mmap(8 * 8) as *i64
395 bkeys2[0] = "kind" as *u8 as i64
396 bkeys2[1] = "pid" as *u8 as i64
397 bkeys2[2] = "tid" as *u8 as i64
398 bkeys2[3] = "due" as *u8 as i64
399 bkeys2[4] = "effort" as *u8 as i64
400 bkeys2[5] = "date" as *u8 as i64
401 bvals2[0] = "bas" as *u8 as i64
402
403 let st: *i64 = sys_mmap(8 * 8) as *i64
404 st[0] = ss_begin() as i64
405 st[1] = sys_mmap(8 * K_MAGIC_2048)
406 st[2] = 0
407 st[3] = 0
408 st[4] = 0
409 st[5] = 0
410 st[6] = prefix as i64
411
412 let t0: *u8 = sys_mmap(128)
413 let f1: *u8 = sys_mmap(128)
414 let f2: *u8 = sys_mmap(128)
415 let f3: *u8 = sys_mmap(128)
416 let f4: *u8 = sys_mmap(256)
417 let f5b: *u8 = sys_mmap(64)
418 let txt: *u8 = sys_mmap(256)
419 var scanned: i64 = 0
420
421 var i: i64 = 0
422 while i < sz {
423 var e: i64 = i
424 var go: i64 = 1
425 while go == 1 {
426 if e >= sz { go = 0 } else {
427 if b[e] == (10 as u8) { go = 0 } else { e = e + 1 }
428 }
429 }
430 var p: i64 = pj_tok(b, i, e, t0, 128)
431 if t0[0] == (35 as u8) { t0[0] = 0 as u8 }
432 if pj_eq(t0, "PROJ" as *u8) == 1 {
433 scanned = scanned + 1
434 p = pj_tok(b, p, e, f1, 128)
435 p = pj_tok(b, p, e, f2, 128)
436 p = pj_tok(b, p, e, f4, 256)
437 if f1[0] == (0 as u8) { pj_p("PROJECT PROJ missing pid -- fail loud\n" as *u8); return 1 }
438 if f2[0] == (0 as u8) { pj_p("PROJECT PROJ missing owner -- fail loud\n" as *u8); return 1 }
439 if f4[0] == (0 as u8) { pj_p("PROJECT PROJ missing title -- fail loud\n" as *u8); return 1 }
440 pvals[1] = f1 as i64
441 pvals[2] = f2 as i64
442 pvals[3] = f4 as i64
443 if pj_put(pkeys, pvals, 4, "prj:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
444 }
445 if pj_eq(t0, "MEMBER" as *u8) == 1 {
446 scanned = scanned + 1
447 p = pj_tok(b, p, e, f1, 128)
448 p = pj_tok(b, p, e, f2, 128)
449 p = pj_tok(b, p, e, f3, 128)
450 p = pj_tok(b, p, e, f4, 256)
451 if f1[0] == (0 as u8) { pj_p("PROJECT MEMBER missing pid -- fail loud\n" as *u8); return 1 }
452 if f2[0] == (0 as u8) { pj_p("PROJECT MEMBER missing person -- fail loud\n" as *u8); return 1 }
453 var rok: i64 = 0
454 if pj_eq(f3, "owner" as *u8) == 1 { rok = 1 }
455 if pj_eq(f3, "team" as *u8) == 1 { rok = 1 }
456 if pj_eq(f3, "contractor" as *u8) == 1 { rok = 1 }
457 if rok == 0 { pj_p("PROJECT MEMBER role must be owner|team|contractor -- fail loud\n" as *u8); return 1 }
458 var cok: i64 = 0
459 if f4[0] == (45 as u8) { if f4[1] == (0 as u8) { cok = 1 } }
460 if cok == 0 { if pj_digits(f4) == 1 { cok = 1 } }
461 if cok == 0 { pj_p("PROJECT MEMBER cap must be minutes or - -- fail loud\n" as *u8); return 1 }
462 mvals[1] = f1 as i64
463 mvals[2] = f2 as i64
464 mvals[3] = f3 as i64
465 mvals[4] = f4 as i64
466 if pj_put(mkeys, mvals, 5, "mem:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
467 }
468 if pj_eq(t0, "BUDGET" as *u8) == 1 {
469 scanned = scanned + 1
470 p = pj_tok(b, p, e, f1, 128)
471 p = pj_tok(b, p, e, f2, 128)
472 if f1[0] == (0 as u8) { pj_p("PROJECT BUDGET missing pid -- fail loud\n" as *u8); return 1 }
473 if pj_digits(f2) == 0 { pj_p("PROJECT BUDGET amount must be cents digits -- fail loud\n" as *u8); return 1 }
474 bvals[1] = f1 as i64
475 bvals[2] = f2 as i64
476 if pj_put(bkeys, bvals, 3, "bud:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
477 }
478 if pj_eq(t0, "COST" as *u8) == 1 {
479 scanned = scanned + 1
480 p = pj_tok(b, p, e, f1, 128)
481 p = pj_tok(b, p, e, f2, 128)
482 p = pj_tok(b, p, e, f3, 128)
483 p = pj_tok(b, p, e, f4, 256)
484 if f1[0] == (0 as u8) { pj_p("PROJECT COST missing pid -- fail loud\n" as *u8); return 1 }
485 if f2[0] == (0 as u8) { pj_p("PROJECT COST missing tid -- fail loud\n" as *u8); return 1 }
486 if pj_digits(f3) == 0 { pj_p("PROJECT COST amount must be cents digits -- fail loud\n" as *u8); return 1 }
487 if pj_datedays(f4) < 0 { pj_p("PROJECT COST bad date -- fail loud\n" as *u8); return 1 }
488 cvals[1] = f1 as i64
489 cvals[2] = f2 as i64
490 cvals[3] = f3 as i64
491 cvals[4] = f4 as i64
492 if pj_put(ckeys, cvals, 5, "cst:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
493 }
494 if pj_eq(t0, "WCONF" as *u8) == 1 {
495 scanned = scanned + 1
496 p = pj_tok(b, p, e, f1, 128)
497 p = pj_tok(b, p, e, f2, 128)
498 p = pj_tok(b, p, e, f3, 128)
499 if f1[0] == (0 as u8) { pj_p("PROJECT WCONF missing pid -- fail loud\n" as *u8); return 1 }
500 if pj_digits(f2) == 0 { pj_p("PROJECT WCONF min-per-day must be digits -- fail loud\n" as *u8); return 1 }
501 if pj_atoi(f2) < 1 { pj_p("PROJECT WCONF min-per-day must be >= 1 -- fail loud\n" as *u8); return 1 }
502 if pj_digits(f3) == 0 { pj_p("PROJECT WCONF tight-days must be digits -- fail loud\n" as *u8); return 1 }
503 wvals[1] = f1 as i64
504 wvals[2] = f2 as i64
505 wvals[3] = f3 as i64
506 if pj_put(wkeys, wvals, 4, "wcf:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
507 }
508 if pj_eq(t0, "LOG" as *u8) == 1 {
509 scanned = scanned + 1
510 p = pj_tok(b, p, e, f1, 128)
511 p = pj_tok(b, p, e, f2, 128)
512 p = pj_tok(b, p, e, f3, 128)
513 p = pj_tok(b, p, e, f4, 256)
514 p = pj_tok(b, p, e, f5b, 64)
515 if f1[0] == (0 as u8) { pj_p("PROJECT LOG missing pid -- fail loud\n" as *u8); return 1 }
516 if f2[0] == (0 as u8) { pj_p("PROJECT LOG missing tid -- fail loud\n" as *u8); return 1 }
517 if f3[0] == (0 as u8) { pj_p("PROJECT LOG missing person -- fail loud\n" as *u8); return 1 }
518 if pj_datedays(f4) < 0 { pj_p("PROJECT LOG bad date -- fail loud\n" as *u8); return 1 }
519 if pj_digits(f5b) == 0 { pj_p("PROJECT LOG minutes must be digits -- fail loud\n" as *u8); return 1 }
520 if pj_atoi(f5b) < 1 { pj_p("PROJECT LOG minutes must be >= 1 -- fail loud\n" as *u8); return 1 }
521 lvals[1] = f1 as i64
522 lvals[2] = f2 as i64
523 lvals[3] = f3 as i64
524 lvals[4] = f4 as i64
525 lvals[5] = f5b as i64
526 if pj_put(lkeys, lvals, 6, "log:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
527 }
528 if pj_eq(t0, "ASSIGN" as *u8) == 1 {
529 scanned = scanned + 1
530 p = pj_tok(b, p, e, f1, 128)
531 p = pj_tok(b, p, e, f2, 128)
532 p = pj_tok(b, p, e, f3, 128)
533 p = pj_tok(b, p, e, f4, 256)
534 if f1[0] == (0 as u8) { pj_p("PROJECT ASSIGN missing pid -- fail loud\n" as *u8); return 1 }
535 if f2[0] == (0 as u8) { pj_p("PROJECT ASSIGN missing tid -- fail loud\n" as *u8); return 1 }
536 if f3[0] == (0 as u8) { pj_p("PROJECT ASSIGN missing person -- fail loud\n" as *u8); return 1 }
537 if pj_datedays(f4) < 0 { pj_p("PROJECT ASSIGN bad date -- fail loud\n" as *u8); return 1 }
538 avals[1] = f1 as i64
539 avals[2] = f2 as i64
540 avals[3] = f3 as i64
541 avals[4] = f4 as i64
542 if pj_put(akeys, avals, 5, "asg:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
543 }
544 if pj_eq(t0, "BASE" as *u8) == 1 {
545 scanned = scanned + 1
546 p = pj_tok(b, p, e, f1, 128)
547 p = pj_tok(b, p, e, f2, 128)
548 p = pj_tok(b, p, e, f3, 128)
549 p = pj_tok(b, p, e, f5b, 64)
550 p = pj_tok(b, p, e, f4, 256)
551 if f1[0] == (0 as u8) { pj_p("PROJECT BASE missing pid -- fail loud\n" as *u8); return 1 }
552 if f2[0] == (0 as u8) { pj_p("PROJECT BASE missing tid -- fail loud\n" as *u8); return 1 }
553 if pj_datedays(f3) < 0 { pj_p("PROJECT BASE bad baseline due date -- fail loud\n" as *u8); return 1 }
554 if pj_digits(f5b) == 0 { pj_p("PROJECT BASE effort must be digits -- fail loud\n" as *u8); return 1 }
555 if pj_datedays(f4) < 0 { pj_p("PROJECT BASE bad snapshot date -- fail loud\n" as *u8); return 1 }
556 bvals2[1] = f1 as i64
557 bvals2[2] = f2 as i64
558 bvals2[3] = f3 as i64
559 bvals2[4] = f5b as i64
560 bvals2[5] = f4 as i64
561 if pj_put(bkeys2, bvals2, 6, "bas:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
562 }
563 if pj_eq(t0, "NOTE" as *u8) == 1 {
564 scanned = scanned + 1
565 p = pj_tok(b, p, e, f1, 128)
566 p = pj_tok(b, p, e, f2, 128)
567 p = pj_tok(b, p, e, f3, 128)
568 p = pj_tok(b, p, e, f4, 256)
569 if f1[0] == (0 as u8) { pj_p("PROJECT NOTE missing pid -- fail loud\n" as *u8); return 1 }
570 if f2[0] == (0 as u8) { pj_p("PROJECT NOTE missing tid -- fail loud\n" as *u8); return 1 }
571 if f3[0] == (0 as u8) { pj_p("PROJECT NOTE missing person -- fail loud\n" as *u8); return 1 }
572 if pj_datedays(f4) < 0 { pj_p("PROJECT NOTE bad date -- fail loud\n" as *u8); return 1 }
573 // text = the rest of the line, whitespace-trimmed both ends (multi-word)
574 var ts: i64 = p
575 var g1: i64 = 1
576 while g1 == 1 {
577 if ts >= e { g1 = 0 } else {
578 if pj_isws(b[ts] as i64) == 1 { ts = ts + 1 } else { g1 = 0 }
579 }
580 }
581 var te: i64 = e
582 var g2: i64 = 1
583 while g2 == 1 {
584 if te <= ts { g2 = 0 } else {
585 if pj_isws(b[te - 1] as i64) == 1 { te = te - 1 } else { g2 = 0 }
586 }
587 }
588 var tn: i64 = 0
589 while ts < te { if tn < 239 { txt[tn] = b[ts]; tn = tn + 1 } ts = ts + 1 }
590 txt[tn] = 0 as u8
591 if txt[0] == (0 as u8) { pj_p("PROJECT NOTE missing text -- fail loud\n" as *u8); return 1 }
592 nvals[1] = f1 as i64
593 nvals[2] = f2 as i64
594 nvals[3] = f3 as i64
595 nvals[4] = f4 as i64
596 nvals[5] = txt as i64
597 if pj_put(nkeys, nvals, 6, "not:" as *u8, st) != 0 { pj_p("PROJECT writer full -- fail loud\n" as *u8); return 1 }
598 }
599 i = e + 1
600 }
601
602 var committed: i64 = 0
603 if st[3] > 0 {
604 if ss_commit(prefix, st[0] as *i64, segid) != 0 { pj_p("PROJECT commit FAILED -- fail loud\n" as *u8); return 1 }
605 committed = 1
606 }
607 let rep: *u8 = sys_mmap(K_MAGIC_1024)
608 var o: i64 = 0
609 o = pj_cat(rep, o, "PROJECT-LOAD scanned=" as *u8)
610 o = pj_catn(rep, o, scanned)
611 o = pj_cat(rep, o, " new=" as *u8)
612 o = pj_catn(rep, o, st[3])
613 o = pj_cat(rep, o, " dup_infile=" as *u8)
614 o = pj_catn(rep, o, st[4])
615 o = pj_cat(rep, o, " dup_instore=" as *u8)
616 o = pj_catn(rep, o, st[5])
617 o = pj_cat(rep, o, " segment=" as *u8)
618 if committed == 1 {
619 o = pj_cat(rep, o, "seg-" as *u8)
620 o = pj_catn(rep, o, segid)
621 } else {
622 o = pj_cat(rep, o, "none" as *u8)
623 }
624 o = pj_cat(rep, o, "\n" as *u8)
625 sys_write(1, rep, o)
626 return 0
627}
628
629// ---------------- GATHER (prj:/mem: + the planner's tsk:/don: for one project) ----------------
630// st slots: 0=ntask 1=tids 2=towners 3=tdeps 4=tefforts(values) 5=ttitles
631// 6=ndone 7=dtids 8=nmem 9=mpersons 10=mroles 11=mcaps
632// 12=projfound 13=projowner 14=projtitle
633// 15=budfound 16=bud_cents 17=ac_cents_sum 18=wcffound 19=wcf_min_per_day 20=wcf_tight_days
634// 21=tdues 22=nlog 23=ltids 24=lpersons 25=lmins(values)
635// 26=nnote 27=ntids 28=npersons 29=ntexts 30=ndates
636// 31=nasg 32=atids 33=apersons 34=adays(values) (effective owner = latest ASSIGN wins)
637// 35=ddates(done dates parallel to dtids) 36=nbase 37=btids 38=bdues 39=befforts(values)
638func pj_st_new() -> *i64 {
639 let st: *i64 = sys_mmap(8 * 48) as *i64
640 st[0] = 0
641 st[1] = sys_mmap(8 * 128)
642 st[2] = sys_mmap(8 * 128)
643 st[3] = sys_mmap(8 * 128)
644 st[4] = sys_mmap(8 * 128)
645 st[5] = sys_mmap(8 * 128)
646 st[6] = 0
647 st[7] = sys_mmap(8 * 256)
648 st[8] = 0
649 st[9] = sys_mmap(8 * 64)
650 st[10] = sys_mmap(8 * 64)
651 st[11] = sys_mmap(8 * 64)
652 st[12] = 0
653 st[13] = 0
654 st[14] = 0
655 st[15] = 0
656 st[16] = 0
657 st[17] = 0
658 st[18] = 0
659 st[19] = 0
660 st[20] = 0
661 st[21] = sys_mmap(8 * 128)
662 st[22] = 0
663 st[23] = sys_mmap(8 * 256)
664 st[24] = sys_mmap(8 * 256)
665 st[25] = sys_mmap(8 * 256)
666 st[26] = 0
667 st[27] = sys_mmap(8 * 128)
668 st[28] = sys_mmap(8 * 128)
669 st[29] = sys_mmap(8 * 128)
670 st[30] = sys_mmap(8 * 128)
671 st[31] = 0
672 st[32] = sys_mmap(8 * 256)
673 st[33] = sys_mmap(8 * 256)
674 st[34] = sys_mmap(8 * 256)
675 st[35] = sys_mmap(8 * 256)
676 st[36] = 0
677 st[37] = sys_mmap(8 * 128)
678 st[38] = sys_mmap(8 * 128)
679 st[39] = sys_mmap(8 * 128)
680 return st
681}
682
683func pj_gather(prefix: *u8, pid: *u8, st: *i64) -> i64 {
684 let segp: *i64 = sys_mmap(16) as *i64
685 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: sees the WHOLE store
686 let segs: *i64 = segp[0] as *i64
687 let fp: *u8 = sys_mmap(128)
688 let fa: *u8 = sys_mmap(256)
689 let fb: *u8 = sys_mmap(256)
690 let fc: *u8 = sys_mmap(256)
691 let fd2: *u8 = sys_mmap(256)
692 let fe: *u8 = sys_mmap(256)
693 let fg: *u8 = sys_mmap(256)
694 var s: i64 = 0
695 while s < ns {
696 let path: *u8 = sys_mmap(512)
697 var po: i64 = 0
698 po = pj_cat(path, po, prefix)
699 po = pj_cat(path, po, segs[s] as *u8)
700 po = pj_cat(path, po, ".docs" as *u8)
701 path[po] = 0 as u8
702 let szp: *i64 = sys_mmap(16) as *i64
703 let b: *u8 = ss_readall(path, szp)
704 let sz: i64 = szp[0]
705 var j: i64 = 0
706 while j + 9 <= sz {
707 let kind: i64 = b[j]
708 let kl: i64 = ss_r32(b, j + 1)
709 let koff: i64 = j + 5
710 let vl: i64 = ss_r32(b, koff + kl)
711 let voff: i64 = koff + kl + 4
712 let rec: *u8 = (b as i64 + voff) as *u8
713 var isprj: i64 = 0
714 var ismem: i64 = 0
715 var istsk: i64 = 0
716 var isdon: i64 = 0
717 var isbud: i64 = 0
718 var iscst: i64 = 0
719 var iswcf: i64 = 0
720 var islog: i64 = 0
721 var isnot: i64 = 0
722 var isasg: i64 = 0
723 var isbas: i64 = 0
724 if kind == 1 { isprj = pj_iskind(b, koff, kl, "prj:" as *u8) }
725 if kind == 1 { ismem = pj_iskind(b, koff, kl, "mem:" as *u8) }
726 if kind == 1 { istsk = pj_iskind(b, koff, kl, "tsk:" as *u8) }
727 if kind == 1 { isdon = pj_iskind(b, koff, kl, "don:" as *u8) }
728 if kind == 1 { isbud = pj_iskind(b, koff, kl, "bud:" as *u8) }
729 if kind == 1 { iscst = pj_iskind(b, koff, kl, "cst:" as *u8) }
730 if kind == 1 { iswcf = pj_iskind(b, koff, kl, "wcf:" as *u8) }
731 if kind == 1 { islog = pj_iskind(b, koff, kl, "log:" as *u8) }
732 if kind == 1 { isnot = pj_iskind(b, koff, kl, "not:" as *u8) }
733 if kind == 1 { isasg = pj_iskind(b, koff, kl, "asg:" as *u8) }
734 if kind == 1 { isbas = pj_iskind(b, koff, kl, "bas:" as *u8) }
735 if isprj == 1 {
736 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 { if st[12] == 0 {
737 pj_field(rec, vl, "owner" as *u8, fa, 256)
738 pj_field(rec, vl, "title" as *u8, fb, 256)
739 let d1: *u8 = pj_dup(fa)
740 let d2: *u8 = pj_dup(fb)
741 st[13] = d1 as i64
742 st[14] = d2 as i64
743 st[12] = 1
744 } } }
745 }
746 if ismem == 1 {
747 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 {
748 pj_field(rec, vl, "person" as *u8, fa, 256)
749 let mp: *i64 = st[9] as *i64
750 if pj_find(mp, st[8], fa) < 0 {
751 if st[8] >= 64 { pj_p("PROJECT gather members full -- fail loud\n" as *u8); return 1 }
752 pj_field(rec, vl, "role" as *u8, fb, 256)
753 pj_field(rec, vl, "cap" as *u8, fc, 256)
754 let mr: *i64 = st[10] as *i64
755 let mc: *i64 = st[11] as *i64
756 let n: i64 = st[8]
757 let d1: *u8 = pj_dup(fa)
758 let d2: *u8 = pj_dup(fb)
759 let d3: *u8 = pj_dup(fc)
760 mp[n] = d1 as i64
761 mr[n] = d2 as i64
762 mc[n] = d3 as i64
763 st[8] = n + 1
764 }
765 } }
766 }
767 if istsk == 1 {
768 if pj_field(rec, vl, "plan" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 {
769 pj_field(rec, vl, "tid" as *u8, fa, 256)
770 let tt: *i64 = st[1] as *i64
771 if pj_find(tt, st[0], fa) < 0 {
772 if st[0] >= 128 { pj_p("PROJECT gather tasks full -- fail loud\n" as *u8); return 1 }
773 pj_field(rec, vl, "owner" as *u8, fb, 256)
774 pj_field(rec, vl, "dep" as *u8, fc, 256)
775 pj_field(rec, vl, "effort" as *u8, fd2, 256)
776 pj_field(rec, vl, "title" as *u8, fe, 256)
777 pj_field(rec, vl, "due" as *u8, fg, 256)
778 let ow: *i64 = st[2] as *i64
779 let dp: *i64 = st[3] as *i64
780 let ef: *i64 = st[4] as *i64
781 let tl: *i64 = st[5] as *i64
782 let du: *i64 = st[21] as *i64
783 let n: i64 = st[0]
784 let d1: *u8 = pj_dup(fa)
785 let d2: *u8 = pj_dup(fb)
786 let d3: *u8 = pj_dup(fc)
787 let d4: *u8 = pj_dup(fe)
788 let d5: *u8 = pj_dup(fg)
789 let ev: i64 = pj_atoi(fd2)
790 tt[n] = d1 as i64
791 ow[n] = d2 as i64
792 dp[n] = d3 as i64
793 ef[n] = ev
794 tl[n] = d4 as i64
795 du[n] = d5 as i64
796 st[0] = n + 1
797 }
798 } }
799 }
800 if isdon == 1 {
801 if pj_field(rec, vl, "plan" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 {
802 pj_field(rec, vl, "tid" as *u8, fa, 256)
803 let dt: *i64 = st[7] as *i64
804 if pj_find(dt, st[6], fa) < 0 {
805 if st[6] >= 256 { pj_p("PROJECT gather done full -- fail loud\n" as *u8); return 1 }
806 pj_field(rec, vl, "date" as *u8, fb, 256)
807 let dd: *i64 = st[35] as *i64
808 let n: i64 = st[6]
809 let d1: *u8 = pj_dup(fa)
810 dt[n] = d1 as i64
811 dd[n] = pj_datedays(fb)
812 st[6] = n + 1
813 }
814 } }
815 }
816 if isbas == 1 {
817 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 {
818 if st[36] >= 128 { pj_p("PROJECT gather baseline full -- fail loud\n" as *u8); return 1 }
819 pj_field(rec, vl, "tid" as *u8, fa, 256)
820 let bt: *i64 = st[37] as *i64
821 if pj_find(bt, st[36], fa) < 0 {
822 pj_field(rec, vl, "due" as *u8, fb, 256)
823 pj_field(rec, vl, "effort" as *u8, fc, 256)
824 let bd: *i64 = st[38] as *i64
825 let be: *i64 = st[39] as *i64
826 let n: i64 = st[36]
827 let d1: *u8 = pj_dup(fa)
828 bt[n] = d1 as i64
829 bd[n] = pj_datedays(fb)
830 be[n] = pj_atoi(fc)
831 st[36] = n + 1
832 }
833 } }
834 }
835 if isbud == 1 {
836 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 { if st[15] == 0 {
837 pj_field(rec, vl, "amount" as *u8, fa, 256)
838 st[16] = pj_atoi(fa)
839 st[15] = 1
840 } } }
841 }
842 if iscst == 1 {
843 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 {
844 pj_field(rec, vl, "amount" as *u8, fa, 256)
845 st[17] = st[17] + pj_atoi(fa)
846 } }
847 }
848 if iswcf == 1 {
849 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 { if st[18] == 0 {
850 pj_field(rec, vl, "mpd" as *u8, fa, 256)
851 pj_field(rec, vl, "tight" as *u8, fb, 256)
852 st[19] = pj_atoi(fa)
853 st[20] = pj_atoi(fb)
854 if st[19] > 0 { st[18] = 1 }
855 } } }
856 }
857 if islog == 1 {
858 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 {
859 if st[22] >= 256 { pj_p("PROJECT gather logs full -- fail loud\n" as *u8); return 1 }
860 pj_field(rec, vl, "tid" as *u8, fa, 256)
861 pj_field(rec, vl, "person" as *u8, fb, 256)
862 pj_field(rec, vl, "min" as *u8, fc, 256)
863 let lt: *i64 = st[23] as *i64
864 let lp: *i64 = st[24] as *i64
865 let lm: *i64 = st[25] as *i64
866 let n: i64 = st[22]
867 let d1: *u8 = pj_dup(fa)
868 let d2: *u8 = pj_dup(fb)
869 let mv: i64 = pj_atoi(fc)
870 lt[n] = d1 as i64
871 lp[n] = d2 as i64
872 lm[n] = mv
873 st[22] = n + 1
874 } }
875 }
876 if isasg == 1 {
877 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 {
878 if st[31] >= 256 { pj_p("PROJECT gather assignments full -- fail loud\n" as *u8); return 1 }
879 pj_field(rec, vl, "tid" as *u8, fa, 256)
880 pj_field(rec, vl, "person" as *u8, fb, 256)
881 pj_field(rec, vl, "date" as *u8, fc, 256)
882 let at2: *i64 = st[32] as *i64
883 let ap2: *i64 = st[33] as *i64
884 let ad2: *i64 = st[34] as *i64
885 let n: i64 = st[31]
886 let d1: *u8 = pj_dup(fa)
887 let d2: *u8 = pj_dup(fb)
888 let dv: i64 = pj_datedays(fc)
889 at2[n] = d1 as i64
890 ap2[n] = d2 as i64
891 ad2[n] = dv
892 st[31] = n + 1
893 } }
894 }
895 if isnot == 1 {
896 if pj_field(rec, vl, "pid" as *u8, fp, 128) == 1 { if pj_eq(fp, pid) == 1 {
897 if st[26] >= 128 { pj_p("PROJECT gather notes full -- fail loud\n" as *u8); return 1 }
898 pj_field(rec, vl, "tid" as *u8, fa, 256)
899 pj_field(rec, vl, "person" as *u8, fb, 256)
900 pj_field(rec, vl, "text" as *u8, fd2, 256)
901 pj_field(rec, vl, "date" as *u8, fe, 256)
902 let nt: *i64 = st[27] as *i64
903 let np: *i64 = st[28] as *i64
904 let nx: *i64 = st[29] as *i64
905 let nd: *i64 = st[30] as *i64
906 let n: i64 = st[26]
907 let d1: *u8 = pj_dup(fa)
908 let d2: *u8 = pj_dup(fb)
909 let d3: *u8 = pj_dup(fd2)
910 let d4: *u8 = pj_dup(fe)
911 nt[n] = d1 as i64
912 np[n] = d2 as i64
913 nx[n] = d3 as i64
914 nd[n] = d4 as i64
915 st[26] = n + 1
916 } }
917 }
918 j = voff + vl
919 }
920 s = s + 1
921 }
922 // effective owner: the LATEST ASSIGN (by date) overrides the task's authored owner.
923 // Tie on date -> lexicographically greater person (deterministic regardless of scan order).
924 if st[31] > 0 {
925 let tt2: *i64 = st[1] as *i64
926 let ow2: *i64 = st[2] as *i64
927 let at3: *i64 = st[32] as *i64
928 let ap3: *i64 = st[33] as *i64
929 let ad3: *i64 = st[34] as *i64
930 var ti: i64 = 0
931 while ti < st[0] {
932 var bestd: i64 = 0 - 1
933 var bestp: i64 = 0
934 var ai: i64 = 0
935 while ai < st[31] {
936 if pj_eq(at3[ai] as *u8, tt2[ti] as *u8) == 1 {
937 var take: i64 = 0
938 if ad3[ai] > bestd { take = 1 }
939 if ad3[ai] == bestd { if bestp != 0 { if pj_cmpgt(ap3[ai] as *u8, bestp as *u8) == 1 { take = 1 } } }
940 if take == 1 { bestd = ad3[ai]; bestp = ap3[ai] }
941 }
942 ai = ai + 1
943 }
944 if bestp != 0 { ow2[ti] = bestp }
945 ti = ti + 1
946 }
947 }
948 return 0
949}
950
951// all deps of a task satisfied? dep field is "-" or "a,b,c"
952func pj_depsok(dep: *u8, dt: *i64, nd: i64) -> i64 {
953 if dep[0] == (45 as u8) { if dep[1] == (0 as u8) { return 1 } }
954 let nm: *u8 = sys_mmap(128)
955 var i: i64 = 0
956 var t: i64 = 0
957 var go: i64 = 1
958 while go == 1 {
959 let c: i64 = dep[i] as i64
960 var fin: i64 = 0
961 if c == 44 { fin = 1 }
962 if c == 0 { fin = 1 }
963 if fin == 1 {
964 nm[t] = 0 as u8
965 if t > 0 { if pj_find(dt, nd, nm) < 0 { return 0 } }
966 t = 0
967 if c == 0 { go = 0 }
968 } else {
969 if t < 127 { nm[t] = dep[i]; t = t + 1 }
970 }
971 i = i + 1
972 }
973 return 1
974}
975
976// 0=READY 1=BLOCKED 2=DONE for task index idx in a gathered st
977func pj_state(st: *i64, idx: i64) -> i64 {
978 let tids: *i64 = st[1] as *i64
979 let deps: *i64 = st[3] as *i64
980 let dt: *i64 = st[7] as *i64
981 let nd: i64 = st[6]
982 if pj_find(dt, nd, tids[idx] as *u8) >= 0 { return 2 }
983 if pj_depsok(deps[idx] as *u8, dt, nd) == 0 { return 1 }
984 return 0
985}
986
987// role of person in a gathered st: project owner is implicitly owner; else the mem: record; 0 = none
988func pj_roleof(st: *i64, person: *u8) -> i64 {
989 if st[12] == 1 { if pj_eq(st[13] as *u8, person) == 1 { return "owner" as *u8 as i64 } }
990 let mp: *i64 = st[9] as *i64
991 let mr: *i64 = st[10] as *i64
992 let idx: i64 = pj_find(mp, st[8], person)
993 if idx >= 0 { return mr[idx] }
994 return 0
995}
996
997func pj_statename(v: i64) -> i64 {
998 if v == 2 { return "DONE" as *u8 as i64 }
999 if v == 1 { return "BLOCKED" as *u8 as i64 }
1000 return "READY" as *u8 as i64
1001}
1002
1003// ---------------- VIEW (fail-closed role-scoped) ----------------
1004// rc: 0 ok, 1 project not found, 2 access denied. out[0]=total tasks, out[1]=shown
1005func pj_view(prefix: *u8, pid: *u8, person: *u8, out: *i64) -> i64 {
1006 out[0] = 0
1007 out[1] = 0
1008 if pj_pathok(prefix) == 0 { return pj_pathfail() }
1009 let st: *i64 = pj_st_new()
1010 if pj_gather(prefix, pid, st) != 0 { return 1 }
1011 let rep: *u8 = sys_mmap(K_MAGIC_2048)
1012 if st[12] == 0 {
1013 var o0: i64 = 0
1014 o0 = pj_cat(rep, o0, "PROJECT-NOTFOUND pid=" as *u8)
1015 o0 = pj_cat(rep, o0, pid)
1016 o0 = pj_cat(rep, o0, " -- fail loud\n" as *u8)
1017 sys_write(1, rep, o0)
1018 return 1
1019 }
1020 let role: i64 = pj_roleof(st, person)
1021 if role == 0 {
1022 var o1: i64 = 0
1023 o1 = pj_cat(rep, o1, "PROJECT-DENY pid=" as *u8)
1024 o1 = pj_cat(rep, o1, pid)
1025 o1 = pj_cat(rep, o1, " person=" as *u8)
1026 o1 = pj_cat(rep, o1, person)
1027 o1 = pj_cat(rep, o1, " -- not a member, access denied by default\n" as *u8)
1028 sys_write(1, rep, o1)
1029 return 2
1030 }
1031 var own_only: i64 = 0
1032 if pj_eq(role as *u8, "contractor" as *u8) == 1 { own_only = 1 }
1033 var o: i64 = 0
1034 o = pj_cat(rep, o, "PROJECT-VIEW pid=" as *u8)
1035 o = pj_cat(rep, o, pid)
1036 o = pj_cat(rep, o, " title=" as *u8)
1037 o = pj_cat(rep, o, st[14] as *u8)
1038 o = pj_cat(rep, o, " person=" as *u8)
1039 o = pj_cat(rep, o, person)
1040 o = pj_cat(rep, o, " role=" as *u8)
1041 o = pj_cat(rep, o, role as *u8)
1042 o = pj_cat(rep, o, " scope=" as *u8)
1043 if own_only == 1 { o = pj_cat(rep, o, "own\n" as *u8) } else { o = pj_cat(rep, o, "all\n" as *u8) }
1044 sys_write(1, rep, o)
1045 let tids: *i64 = st[1] as *i64
1046 let owners: *i64 = st[2] as *i64
1047 let titles: *i64 = st[5] as *i64
1048 let efforts: *i64 = st[4] as *i64
1049 var shown: i64 = 0
1050 var i: i64 = 0
1051 while i < st[0] {
1052 var vis: i64 = 1
1053 if own_only == 1 { if pj_eq(owners[i] as *u8, person) == 0 { vis = 0 } }
1054 if vis == 1 {
1055 let sv: i64 = pj_state(st, i)
1056 let sn: i64 = pj_statename(sv)
1057 var t: i64 = 0
1058 t = pj_cat(rep, t, "PROJECT-TASK tid=" as *u8)
1059 t = pj_cat(rep, t, tids[i] as *u8)
1060 t = pj_cat(rep, t, " owner=" as *u8)
1061 t = pj_cat(rep, t, owners[i] as *u8)
1062 t = pj_cat(rep, t, " state=" as *u8)
1063 t = pj_cat(rep, t, sn as *u8)
1064 t = pj_cat(rep, t, " effort_min=" as *u8)
1065 t = pj_catn(rep, t, efforts[i])
1066 t = pj_cat(rep, t, " title=" as *u8)
1067 t = pj_cat(rep, t, titles[i] as *u8)
1068 t = pj_cat(rep, t, "\n" as *u8)
1069 sys_write(1, rep, t)
1070 shown = shown + 1
1071 }
1072 i = i + 1
1073 }
1074 var z: i64 = 0
1075 z = pj_cat(rep, z, "PROJECT-VIEW-END total=" as *u8)
1076 z = pj_catn(rep, z, st[0])
1077 z = pj_cat(rep, z, " shown=" as *u8)
1078 z = pj_catn(rep, z, shown)
1079 z = pj_cat(rep, z, "\n" as *u8)
1080 sys_write(1, rep, z)
1081 out[0] = st[0]
1082 out[1] = shown
1083 return 0
1084}
1085
1086// per-person open effort (not-done tasks owned by person); -1 if project missing
1087func pj_open_min(prefix: *u8, pid: *u8, person: *u8) -> i64 {
1088 let st: *i64 = pj_st_new()
1089 if pj_gather(prefix, pid, st) != 0 { return 0 - 1 }
1090 if st[12] == 0 { return 0 - 1 }
1091 let owners: *i64 = st[2] as *i64
1092 let efforts: *i64 = st[4] as *i64
1093 var sum: i64 = 0
1094 var i: i64 = 0
1095 while i < st[0] {
1096 if pj_eq(owners[i] as *u8, person) == 1 {
1097 let sv: i64 = pj_state(st, i)
1098 if sv != 2 { sum = sum + efforts[i] }
1099 }
1100 i = i + 1
1101 }
1102 return sum
1103}
1104
1105// over-capacity: 1 over, 0 within, -1 no recorded capacity (or not a member)
1106func pj_over(prefix: *u8, pid: *u8, person: *u8) -> i64 {
1107 let st: *i64 = pj_st_new()
1108 if pj_gather(prefix, pid, st) != 0 { return 0 - 1 }
1109 let mp: *i64 = st[9] as *i64
1110 let mc: *i64 = st[11] as *i64
1111 let idx: i64 = pj_find(mp, st[8], person)
1112 if idx < 0 { return 0 - 1 }
1113 let cap: *u8 = mc[idx] as *u8
1114 if pj_digits(cap) == 0 { return 0 - 1 }
1115 let open: i64 = pj_open_min(prefix, pid, person)
1116 if open > pj_atoi(cap) { return 1 }
1117 return 0
1118}
1119
1120// task visibility for a person: 1 visible, 0 hidden, -1 no such task, -2 denied
1121func pj_visible(prefix: *u8, pid: *u8, person: *u8, tid: *u8) -> i64 {
1122 let st: *i64 = pj_st_new()
1123 if pj_gather(prefix, pid, st) != 0 { return 0 - 2 }
1124 if st[12] == 0 { return 0 - 2 }
1125 let role: i64 = pj_roleof(st, person)
1126 if role == 0 { return 0 - 2 }
1127 let tids: *i64 = st[1] as *i64
1128 let idx: i64 = pj_find(tids, st[0], tid)
1129 if idx < 0 { return 0 - 1 }
1130 if pj_eq(role as *u8, "contractor" as *u8) == 1 {
1131 let owners: *i64 = st[2] as *i64
1132 if pj_eq(owners[idx] as *u8, person) == 0 { return 0 }
1133 }
1134 return 1
1135}
1136
1137// per-task state by id: -1 missing, else pj_state code
1138func pj_task_state(prefix: *u8, pid: *u8, tid: *u8) -> i64 {
1139 let st: *i64 = pj_st_new()
1140 if pj_gather(prefix, pid, st) != 0 { return 0 - 1 }
1141 let tids: *i64 = st[1] as *i64
1142 let idx: i64 = pj_find(tids, st[0], tid)
1143 if idx < 0 { return 0 - 1 }
1144 return pj_state(st, idx)
1145}
1146
1147// open critical-chain minutes to finish task idx (own effort + deepest unfinished dep chain);
1148// -1 = dependency cycle (loud upstream). Done tasks contribute 0.
1149func pj_chain_min(st: *i64, idx: i64, depth: i64) -> i64 {
1150 if depth > st[0] { return 0 - 1 }
1151 if pj_state(st, idx) == 2 { return 0 }
1152 let tids: *i64 = st[1] as *i64
1153 let deps: *i64 = st[3] as *i64
1154 let efforts: *i64 = st[4] as *i64
1155 let dep: *u8 = deps[idx] as *u8
1156 var mx: i64 = 0
1157 var isdash: i64 = 0
1158 if dep[0] == (45 as u8) { if dep[1] == (0 as u8) { isdash = 1 } }
1159 if isdash == 0 {
1160 let nm: *u8 = sys_mmap(128)
1161 var i: i64 = 0
1162 var t: i64 = 0
1163 var go: i64 = 1
1164 while go == 1 {
1165 let c: i64 = dep[i] as i64
1166 var fin: i64 = 0
1167 if c == 44 { fin = 1 }
1168 if c == 0 { fin = 1 }
1169 if fin == 1 {
1170 nm[t] = 0 as u8
1171 if t > 0 {
1172 let di: i64 = pj_find(tids, st[0], nm)
1173 if di >= 0 {
1174 let cm: i64 = pj_chain_min(st, di, depth + 1)
1175 if cm < 0 { return 0 - 1 }
1176 if cm > mx { mx = cm }
1177 }
1178 }
1179 t = 0
1180 if c == 0 { go = 0 }
1181 } else {
1182 if t < 127 { nm[t] = dep[i]; t = t + 1 }
1183 }
1184 i = i + 1
1185 }
1186 }
1187 return efforts[idx] + mx
1188}
1189
1190// ---------------- LEVEL (workload leveling; aply="-" = advisory, aply=<date> = APPLY as ASSIGN records) ----------------
1191// out[0]=moves, out[1]=members still over capacity after the moves
1192func pj_cmd_level(prefix: *u8, pid: *u8, out: *i64, aply: *u8) -> i64 {
1193 out[0] = 0
1194 out[1] = 0
1195 if pj_pathok(prefix) == 0 { return pj_pathfail() }
1196 var apdays: i64 = 0 - 1
1197 if pj_eq(aply, "-" as *u8) == 0 {
1198 apdays = pj_datedays(aply)
1199 if apdays < 0 { pj_p("PROJECT level apply bad date -- fail loud\n" as *u8); return 1 }
1200 }
1201 let akeys2: *i64 = sys_mmap(8 * 8) as *i64
1202 let avals2: *i64 = sys_mmap(8 * 8) as *i64
1203 akeys2[0] = "kind" as *u8 as i64
1204 akeys2[1] = "pid" as *u8 as i64
1205 akeys2[2] = "tid" as *u8 as i64
1206 akeys2[3] = "person" as *u8 as i64
1207 akeys2[4] = "date" as *u8 as i64
1208 avals2[0] = "asg" as *u8 as i64
1209 let ast: *i64 = sys_mmap(8 * 8) as *i64
1210 if apdays >= 0 {
1211 ast[0] = ss_begin() as i64
1212 ast[1] = sys_mmap(8 * K_MAGIC_2048)
1213 ast[2] = 0
1214 ast[3] = 0
1215 ast[4] = 0
1216 ast[5] = 0
1217 ast[6] = prefix as i64
1218 }
1219 let st: *i64 = pj_st_new()
1220 if pj_gather(prefix, pid, st) != 0 { return 1 }
1221 let rep: *u8 = sys_mmap(K_MAGIC_2048)
1222 if st[12] == 0 {
1223 var o0: i64 = 0
1224 o0 = pj_cat(rep, o0, "PROJECT-NOTFOUND pid=" as *u8)
1225 o0 = pj_cat(rep, o0, pid)
1226 o0 = pj_cat(rep, o0, " -- fail loud\n" as *u8)
1227 sys_write(1, rep, o0)
1228 return 1
1229 }
1230 let mp: *i64 = st[9] as *i64
1231 let mc: *i64 = st[11] as *i64
1232 let owners: *i64 = st[2] as *i64
1233 let efforts: *i64 = st[4] as *i64
1234 let tids: *i64 = st[1] as *i64
1235 let tst: *i64 = sys_mmap(8 * 128) as *i64
1236 var i: i64 = 0
1237 while i < st[0] {
1238 let sv: i64 = pj_state(st, i)
1239 tst[i] = sv
1240 i = i + 1
1241 }
1242 let loads: *i64 = sys_mmap(8 * 64) as *i64
1243 var m: i64 = 0
1244 while m < st[8] {
1245 var sum: i64 = 0
1246 i = 0
1247 while i < st[0] {
1248 if pj_eq(owners[i] as *u8, mp[m] as *u8) == 1 { if tst[i] != 2 { sum = sum + efforts[i] } }
1249 i = i + 1
1250 }
1251 loads[m] = sum
1252 m = m + 1
1253 }
1254 let moved: *i64 = sys_mmap(8 * 128) as *i64
1255 var moves: i64 = 0
1256 m = 0
1257 while m < st[8] {
1258 var capm: i64 = 0 - 1
1259 if pj_digits(mc[m] as *u8) == 1 { capm = pj_atoi(mc[m] as *u8) }
1260 if capm >= 0 {
1261 var moving: i64 = 1
1262 var giter: i64 = 0
1263 while moving == 1 {
1264 if loads[m] <= capm { moving = 0 } else {
1265 giter = giter + 1
1266 if giter > st[0] + 1 { moving = 0 } else {
1267 var found: i64 = 0 - 1
1268 i = 0
1269 while i < st[0] {
1270 if found < 0 { if moved[i] == 0 { if tst[i] == 0 { if pj_eq(owners[i] as *u8, mp[m] as *u8) == 1 { found = i } } } }
1271 i = i + 1
1272 }
1273 if found < 0 {
1274 var s1: i64 = 0
1275 s1 = pj_cat(rep, s1, "PROJECT-LEVEL-STUCK person=" as *u8)
1276 s1 = pj_cat(rep, s1, mp[m] as *u8)
1277 s1 = pj_cat(rep, s1, " over_min=" as *u8)
1278 s1 = pj_catn(rep, s1, loads[m] - capm)
1279 s1 = pj_cat(rep, s1, " -- no READY task to move (blocked work cannot be leveled)\n" as *u8)
1280 sys_write(1, rep, s1)
1281 moving = 0
1282 } else {
1283 let e: i64 = efforts[found]
1284 var best: i64 = 0 - 1
1285 var besthr: i64 = 0 - 1
1286 var t2: i64 = 0
1287 while t2 < st[8] {
1288 if t2 != m { if pj_digits(mc[t2] as *u8) == 1 {
1289 let hr: i64 = pj_atoi(mc[t2] as *u8) - loads[t2]
1290 if hr >= e { if hr > besthr { best = t2; besthr = hr } }
1291 } }
1292 t2 = t2 + 1
1293 }
1294 if best < 0 {
1295 moved[found] = 1
1296 var s2: i64 = 0
1297 s2 = pj_cat(rep, s2, "PROJECT-LEVEL-NOTARGET tid=" as *u8)
1298 s2 = pj_cat(rep, s2, tids[found] as *u8)
1299 s2 = pj_cat(rep, s2, " -- no member has recorded headroom for it\n" as *u8)
1300 sys_write(1, rep, s2)
1301 } else {
1302 var s3: i64 = 0
1303 s3 = pj_cat(rep, s3, "PROJECT-LEVEL move tid=" as *u8)
1304 s3 = pj_cat(rep, s3, tids[found] as *u8)
1305 s3 = pj_cat(rep, s3, " from=" as *u8)
1306 s3 = pj_cat(rep, s3, mp[m] as *u8)
1307 s3 = pj_cat(rep, s3, " to=" as *u8)
1308 s3 = pj_cat(rep, s3, mp[best] as *u8)
1309 s3 = pj_cat(rep, s3, " effort_min=" as *u8)
1310 s3 = pj_catn(rep, s3, e)
1311 s3 = pj_cat(rep, s3, "\n" as *u8)
1312 sys_write(1, rep, s3)
1313 if apdays >= 0 {
1314 avals2[1] = pid as i64
1315 avals2[2] = tids[found]
1316 avals2[3] = mp[best]
1317 avals2[4] = aply as i64
1318 if pj_put(akeys2, avals2, 5, "asg:" as *u8, ast) != 0 { pj_p("PROJECT level apply writer full -- fail loud\n" as *u8); return 1 }
1319 }
1320 loads[m] = loads[m] - e
1321 loads[best] = loads[best] + e
1322 moved[found] = 1
1323 moves = moves + 1
1324 }
1325 }
1326 }
1327 }
1328 }
1329 }
1330 m = m + 1
1331 }
1332 var over_after: i64 = 0
1333 m = 0
1334 while m < st[8] {
1335 if pj_digits(mc[m] as *u8) == 1 { if loads[m] > pj_atoi(mc[m] as *u8) { over_after = over_after + 1 } }
1336 m = m + 1
1337 }
1338 var z: i64 = 0
1339 z = pj_cat(rep, z, "PROJECT-LEVEL-END moves=" as *u8)
1340 z = pj_catn(rep, z, moves)
1341 z = pj_cat(rep, z, " over_after=" as *u8)
1342 z = pj_catn(rep, z, over_after)
1343 if apdays >= 0 { z = pj_cat(rep, z, " mode=APPLY\n" as *u8) } else { z = pj_cat(rep, z, " mode=advisory (records untouched)\n" as *u8) }
1344 sys_write(1, rep, z)
1345 if apdays >= 0 {
1346 if ast[3] > 0 {
1347 if ss_commit(prefix, ast[0] as *i64, sys_now_us()) != 0 { pj_p("PROJECT level apply commit FAILED -- fail loud\n" as *u8); return 1 }
1348 }
1349 var z2: i64 = 0
1350 z2 = pj_cat(rep, z2, "PROJECT-LEVEL-APPLIED assigns=" as *u8)
1351 z2 = pj_catn(rep, z2, ast[3])
1352 z2 = pj_cat(rep, z2, " date=" as *u8)
1353 z2 = pj_cat(rep, z2, aply)
1354 z2 = pj_cat(rep, z2, " -- ASSIGN records on the additive plane (latest wins)\n" as *u8)
1355 sys_write(1, rep, z2)
1356 }
1357 out[0] = moves
1358 out[1] = over_after
1359 return 0
1360}
1361
1362// ---------------- RISK (register + delay prediction; duration risk only when WCONF is recorded) ----------------
1363// out: 0=overdue 1=late 2=tight 3=ok 4=unrated
1364func pj_cmd_risk(prefix: *u8, pid: *u8, today: *u8, out: *i64) -> i64 {
1365 out[0] = 0
1366 out[1] = 0
1367 out[2] = 0
1368 out[3] = 0
1369 out[4] = 0
1370 if pj_pathok(prefix) == 0 { return pj_pathfail() }
1371 let td: i64 = pj_datedays(today)
1372 if td < 0 { pj_p("PROJECT risk bad today date -- fail loud\n" as *u8); return 1 }
1373 let st: *i64 = pj_st_new()
1374 if pj_gather(prefix, pid, st) != 0 { return 1 }
1375 let rep: *u8 = sys_mmap(K_MAGIC_2048)
1376 if st[12] == 0 {
1377 var o0: i64 = 0
1378 o0 = pj_cat(rep, o0, "PROJECT-NOTFOUND pid=" as *u8)
1379 o0 = pj_cat(rep, o0, pid)
1380 o0 = pj_cat(rep, o0, " -- fail loud\n" as *u8)
1381 sys_write(1, rep, o0)
1382 return 1
1383 }
1384 let tids: *i64 = st[1] as *i64
1385 let dues: *i64 = st[21] as *i64
1386 var i: i64 = 0
1387 while i < st[0] {
1388 let sv: i64 = pj_state(st, i)
1389 if sv != 2 {
1390 let dd: i64 = pj_datedays(dues[i] as *u8)
1391 if dd < 0 { pj_p("PROJECT risk bad stored due date -- fail loud\n" as *u8); return 1 }
1392 let dl: i64 = dd - td
1393 var t: i64 = 0
1394 t = pj_cat(rep, t, "PROJECT-RISK tid=" as *u8)
1395 t = pj_cat(rep, t, tids[i] as *u8)
1396 t = pj_cat(rep, t, " due=" as *u8)
1397 t = pj_cat(rep, t, dues[i] as *u8)
1398 t = pj_cat(rep, t, " days_left=" as *u8)
1399 t = pj_catn(rep, t, dl)
1400 if dl < 0 {
1401 out[0] = out[0] + 1
1402 t = pj_cat(rep, t, " level=OVERDUE" as *u8)
1403 } else {
1404 if st[18] == 1 {
1405 let cm: i64 = pj_chain_min(st, i, 0)
1406 if cm < 0 { pj_p("\nPROJECT-RISK-CYCLE dependency cycle -- fail loud\n" as *u8); return 1 }
1407 let dn: i64 = (cm + st[19] - 1) / st[19]
1408 let slack: i64 = dl - dn
1409 t = pj_cat(rep, t, " chain_min=" as *u8)
1410 t = pj_catn(rep, t, cm)
1411 t = pj_cat(rep, t, " days_needed=" as *u8)
1412 t = pj_catn(rep, t, dn)
1413 t = pj_cat(rep, t, " slack_days=" as *u8)
1414 t = pj_catn(rep, t, slack)
1415 if slack < 0 {
1416 out[1] = out[1] + 1
1417 t = pj_cat(rep, t, " level=LATE" as *u8)
1418 } else {
1419 if slack <= st[20] {
1420 out[2] = out[2] + 1
1421 t = pj_cat(rep, t, " level=TIGHT" as *u8)
1422 } else {
1423 out[3] = out[3] + 1
1424 t = pj_cat(rep, t, " level=OK" as *u8)
1425 }
1426 }
1427 } else {
1428 out[4] = out[4] + 1
1429 t = pj_cat(rep, t, " level=UNRATED (no WCONF min-per-day recorded)" as *u8)
1430 }
1431 }
1432 t = pj_cat(rep, t, "\n" as *u8)
1433 sys_write(1, rep, t)
1434 }
1435 i = i + 1
1436 }
1437 var z: i64 = 0
1438 z = pj_cat(rep, z, "PROJECT-RISK-END overdue=" as *u8)
1439 z = pj_catn(rep, z, out[0])
1440 z = pj_cat(rep, z, " late=" as *u8)
1441 z = pj_catn(rep, z, out[1])
1442 z = pj_cat(rep, z, " tight=" as *u8)
1443 z = pj_catn(rep, z, out[2])
1444 z = pj_cat(rep, z, " ok=" as *u8)
1445 z = pj_catn(rep, z, out[3])
1446 z = pj_cat(rep, z, " unrated=" as *u8)
1447 z = pj_catn(rep, z, out[4])
1448 z = pj_cat(rep, z, "\n" as *u8)
1449 sys_write(1, rep, z)
1450 return 0
1451}
1452
1453// ---------------- EVM (budget / cost / earned value; integer permil, no floats) ----------------
1454// out: 0=scope_done_permil 1=cpi_permil(-1 when no spend) 2=over_budget
1455func pj_cmd_evm(prefix: *u8, pid: *u8, out: *i64) -> i64 {
1456 out[0] = 0
1457 out[1] = 0 - 1
1458 out[2] = 0
1459 if pj_pathok(prefix) == 0 { return pj_pathfail() }
1460 let st: *i64 = pj_st_new()
1461 if pj_gather(prefix, pid, st) != 0 { return 1 }
1462 let rep: *u8 = sys_mmap(K_MAGIC_1024)
1463 if st[12] == 0 {
1464 var o0: i64 = 0
1465 o0 = pj_cat(rep, o0, "PROJECT-NOTFOUND pid=" as *u8)
1466 o0 = pj_cat(rep, o0, pid)
1467 o0 = pj_cat(rep, o0, " -- fail loud\n" as *u8)
1468 sys_write(1, rep, o0)
1469 return 1
1470 }
1471 if st[15] == 0 { pj_p("PROJECT evm: no BUDGET recorded for this project -- fail loud\n" as *u8); return 1 }
1472 let efforts: *i64 = st[4] as *i64
1473 var total: i64 = 0
1474 var done: i64 = 0
1475 var i: i64 = 0
1476 while i < st[0] {
1477 total = total + efforts[i]
1478 let sv: i64 = pj_state(st, i)
1479 if sv == 2 { done = done + efforts[i] }
1480 i = i + 1
1481 }
1482 if total == 0 { pj_p("PROJECT evm: no tasks recorded -- fail loud\n" as *u8); return 1 }
1483 let scope: i64 = done * 1000 / total
1484 let bac: i64 = st[16]
1485 let ac: i64 = st[17]
1486 let ev: i64 = bac * scope / 1000
1487 var cpi: i64 = 0 - 1
1488 if ac > 0 { cpi = ev * 1000 / ac }
1489 var over: i64 = 0
1490 if ac > bac { over = 1 }
1491 var z: i64 = 0
1492 z = pj_cat(rep, z, "PROJECT-EVM bac_cents=" as *u8)
1493 z = pj_catn(rep, z, bac)
1494 z = pj_cat(rep, z, " ac_cents=" as *u8)
1495 z = pj_catn(rep, z, ac)
1496 z = pj_cat(rep, z, " scope_done_permil=" as *u8)
1497 z = pj_catn(rep, z, scope)
1498 z = pj_cat(rep, z, " earned_value_cents=" as *u8)
1499 z = pj_catn(rep, z, ev)
1500 z = pj_cat(rep, z, " cpi_permil=" as *u8)
1501 if cpi < 0 { z = pj_cat(rep, z, "-" as *u8) } else { z = pj_catn(rep, z, cpi) }
1502 z = pj_cat(rep, z, " over_budget=" as *u8)
1503 z = pj_catn(rep, z, over)
1504 z = pj_cat(rep, z, "\n" as *u8)
1505 sys_write(1, rep, z)
1506 out[0] = scope
1507 out[1] = cpi
1508 out[2] = over
1509 return 0
1510}
1511
1512// ---------------- HTML (organ-emitted 0-JS scoped page; the /project surface) ----------------
1513// pj_head/pj_foot = the shared APP SHELL at the fleet UI bar (feedback-ui-quality-is-not-presence):
1514// canonical --nx- tokens + dark mode (prefers-color-scheme) + clamp() fluid type + skip-link +
1515// header/nav/main landmarks + :focus-visible + reduced-motion + entrance stagger + tablewrap
1516// (wide tables scroll INSIDE their container -- the page body never scrolls sideways on a phone).
1517func pj_head(hb: *u8, o0: i64, title: *u8) -> i64 {
1518 var o: i64 = o0
1519 o = pj_cat(hb, o, "<" as *u8)
1520 hb[o] = 33 as u8
1521 o = o + 1
1522 o = pj_cat(hb, o, "DOCTYPE html><html lang=\"en\"><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'><title>" as *u8)
1523 o = pj_cat(hb, o, title)
1524 o = pj_cat(hb, o, "</title><style>:root{--nx-color-bg:rgb(247,248,250);--nx-color-surface:rgb(255,255,255);--nx-color-ink:rgb(24,28,35);--nx-color-muted:rgb(95,102,112);--nx-color-line:rgb(216,220,228);--nx-color-accent:rgb(11,84,196);--nx-color-ok:rgb(18,122,62);--nx-color-warn:rgb(176,108,14);--nx-color-bad:rgb(178,38,38);--nx-radius:10px}@media (prefers-color-scheme:dark){:root{--nx-color-bg:rgb(16,18,22);--nx-color-surface:rgb(26,29,35);--nx-color-ink:rgb(232,235,240);--nx-color-muted:rgb(150,157,167);--nx-color-line:rgb(58,63,72);--nx-color-accent:rgb(112,164,248);--nx-color-ok:rgb(96,196,136);--nx-color-warn:rgb(230,174,84);--nx-color-bad:rgb(240,126,126)}}*{box-sizing:border-box}body{font-family:system-ui,sans-serif;background:var(--nx-color-bg);color:var(--nx-color-ink);margin:0;font-size:clamp(15px,1vw + 12px,17px);line-height:1.55}.skip-link{position:absolute;left:-999px;top:0;background:var(--nx-color-accent);color:rgb(255,255,255);padding:12px 16px;border-radius:0 0 8px 0;z-index:9}.skip-link:focus{left:0}.hd{background:linear-gradient(90deg,var(--nx-color-accent),rgb(9,58,136))}.hd nav{display:flex;align-items:center;gap:10px;max-width:1040px;margin:0 auto;padding:8px clamp(12px,4vw,28px)}.hd a{color:rgb(255,255,255);text-decoration:none;padding:10px 14px;border-radius:8px;font-weight:600;min-height:44px;display:inline-flex;align-items:center}.hd a:hover{background:rgba(255,255,255,0.16)}.hd svg{color:rgb(255,255,255)}main{max-width:1040px;margin:0 auto;padding:clamp(14px,3vw,28px)}h1{font-size:clamp(22px,3vw,30px);margin:12px 0 6px}h2{font-size:clamp(16px,2vw,19px);margin:26px 0 8px}.meta{color:var(--nx-color-muted);font-size:13px}.tablewrap{overflow-x:auto;background:var(--nx-color-surface);border:1px solid var(--nx-color-line);border-radius:var(--nx-radius);box-shadow:0 1px 3px rgba(16,20,28,0.08)}table{border-collapse:collapse;width:100%;min-width:640px}td,th{border-bottom:1px solid var(--nx-color-line);padding:10px 12px;text-align:left;font-size:14px}th{color:var(--nx-color-muted);font-weight:600;text-transform:uppercase;font-size:12px;letter-spacing:0.4px}tr:hover td{background:rgba(11,84,196,0.06)}.DONE{color:var(--nx-color-ok);font-weight:700}.BLOCKED{color:var(--nx-color-bad);font-weight:700}.READY{color:var(--nx-color-accent);font-weight:700}.warn{color:var(--nx-color-warn);font-weight:700}@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}main>*{animation:rise 0.5s ease both}main>*:nth-child(2){animation-delay:0.05s}main>*:nth-child(3){animation-delay:0.1s}main>*:nth-child(4){animation-delay:0.15s}main>*:nth-child(5){animation-delay:0.2s}@media (prefers-reduced-motion:reduce){main>*{animation:none}}:focus-visible{outline:3px solid var(--nx-color-accent);outline-offset:2px}.gr{display:flex;align-items:center;margin:6px 0}.gl{width:70px;font-size:12px;color:var(--nx-color-muted)}.gwrap{flex:1;background:var(--nx-color-surface);border:1px solid var(--nx-color-line);border-radius:6px}.gb{height:14px;border-radius:6px}.gbR{background:var(--nx-color-accent)}.gbB{background:var(--nx-color-bad)}.gbD{background:var(--nx-color-ok)}a.card{display:block;padding:16px;margin:10px 0;background:var(--nx-color-surface);border:1px solid var(--nx-color-line);border-radius:var(--nx-radius);color:var(--nx-color-ink);text-decoration:none;box-shadow:0 1px 3px rgba(16,20,28,0.08)}a.card:hover{border-color:var(--nx-color-accent);box-shadow:0 4px 14px rgba(11,84,196,0.15)}@media (max-width:720px){td,th{padding:8px;font-size:13px}.gl{width:52px}}</style></head><body><a class='skip-link' href='" as *u8)
1525 hb[o] = 35 as u8
1526 o = o + 1
1527 o = pj_cat(hb, o, "main'>Skip to content</a><header class='hd'><nav><svg viewBox='0 0 24 24' width='22' height='22' aria-hidden='true'><circle cx='12' cy='12' r='9' fill='none' stroke='currentColor' stroke-width='2'/><path d='M7 12h10M12 7v10' stroke='currentColor' stroke-width='2'/></svg><a href='/project'>Projects</a><a href='/project/demo'>Demo</a></nav></header><main id='main'>" as *u8)
1528 return o
1529}
1530
1531func pj_foot(hb: *u8, o0: i64) -> i64 {
1532 var o: i64 = o0
1533 o = pj_cat(hb, o, "<p class='meta'>Generated by nx_project - sovereign 0-JS - additive never-lose records - access is role-scoped fail-closed</p></main></body></html>" as *u8)
1534 return o
1535}
1536// build the page into caller buffer hb; returns length, or -1 notfound, -2 deny, -3 bad path/date.
1537// SILENT on failure (the gateway daemon chooses the HTTP response); the CLI wrapper prints loud.
1538func pj_html_build(prefix: *u8, pid: *u8, person: *u8, today: *u8, hb: *u8) -> i64 {
1539 if pj_pathok(prefix) == 0 { return 0 - 3 }
1540 let td: i64 = pj_datedays(today)
1541 if td < 0 { return 0 - 3 }
1542 let st: *i64 = pj_st_new()
1543 if pj_gather(prefix, pid, st) != 0 { return 0 - 1 }
1544 if st[12] == 0 { return 0 - 1 }
1545 let role: i64 = pj_roleof(st, person)
1546 if role == 0 { return 0 - 2 }
1547 var own_only: i64 = 0
1548 if pj_eq(role as *u8, "contractor" as *u8) == 1 { own_only = 1 }
1549 let ttl: *u8 = sys_mmap(256)
1550 var to: i64 = 0
1551 to = pj_cat(ttl, to, "Nishi Project - " as *u8)
1552 to = pj_cat(ttl, to, pid)
1553 ttl[to] = 0 as u8
1554 var o: i64 = 0
1555 o = pj_head(hb, o, ttl)
1556 o = pj_cat(hb, o, "<h1>Nishi Project - " as *u8)
1557 o = pj_cat(hb, o, st[14] as *u8)
1558 o = pj_cat(hb, o, "</h1><p class='meta'>project=" as *u8)
1559 o = pj_cat(hb, o, pid)
1560 o = pj_cat(hb, o, " viewer=" as *u8)
1561 o = pj_cat(hb, o, person)
1562 o = pj_cat(hb, o, " role=" as *u8)
1563 o = pj_cat(hb, o, role as *u8)
1564 o = pj_cat(hb, o, " scope=" as *u8)
1565 if own_only == 1 { o = pj_cat(hb, o, "own-tasks-only" as *u8) } else { o = pj_cat(hb, o, "all-tasks" as *u8) }
1566 o = pj_cat(hb, o, " as-of=" as *u8)
1567 o = pj_cat(hb, o, today)
1568 o = pj_cat(hb, o, "</p><h2>Tasks</h2><div class='tablewrap'><table><tr><th>task</th><th>owner</th><th>state</th><th>due</th><th>days left</th><th>risk</th><th>effort min</th><th>title</th></tr>" as *u8)
1569 let tids: *i64 = st[1] as *i64
1570 let owners: *i64 = st[2] as *i64
1571 let titles: *i64 = st[5] as *i64
1572 let efforts: *i64 = st[4] as *i64
1573 let dues: *i64 = st[21] as *i64
1574 var r_ov: i64 = 0
1575 var r_la: i64 = 0
1576 var r_ti: i64 = 0
1577 var r_ok: i64 = 0
1578 var i: i64 = 0
1579 while i < st[0] {
1580 var vis: i64 = 1
1581 if own_only == 1 { if pj_eq(owners[i] as *u8, person) == 0 { vis = 0 } }
1582 if vis == 1 {
1583 let sv: i64 = pj_state(st, i)
1584 let sn: i64 = pj_statename(sv)
1585 o = pj_cat(hb, o, "<tr><td>" as *u8)
1586 o = pj_cat(hb, o, tids[i] as *u8)
1587 o = pj_cat(hb, o, "</td><td>" as *u8)
1588 o = pj_cat(hb, o, owners[i] as *u8)
1589 o = pj_cat(hb, o, "</td><td class='" as *u8)
1590 o = pj_cat(hb, o, sn as *u8)
1591 o = pj_cat(hb, o, "'>" as *u8)
1592 o = pj_cat(hb, o, sn as *u8)
1593 o = pj_cat(hb, o, "</td><td>" as *u8)
1594 o = pj_cat(hb, o, dues[i] as *u8)
1595 o = pj_cat(hb, o, "</td><td>" as *u8)
1596 let dd: i64 = pj_datedays(dues[i] as *u8)
1597 if dd >= 0 { o = pj_catn(hb, o, dd - td) } else { o = pj_cat(hb, o, "-" as *u8) }
1598 o = pj_cat(hb, o, "</td><td>" as *u8)
1599 // risk cell: OVERDUE by pure dates; LATE/TIGHT/OK by chain-slack when WCONF is recorded
1600 var rlvl: i64 = 0 - 1
1601 if sv != 2 { if dd >= 0 {
1602 let dl2: i64 = dd - td
1603 if dl2 < 0 { rlvl = 3 } else {
1604 if st[18] == 1 {
1605 let cm: i64 = pj_chain_min(st, i, 0)
1606 if cm < 0 { rlvl = 4 } else {
1607 let dnd: i64 = (cm + st[19] - 1) / st[19]
1608 let sl: i64 = dl2 - dnd
1609 if sl < 0 { rlvl = 2 } else {
1610 if sl <= st[20] { rlvl = 1 } else { rlvl = 0 }
1611 }
1612 }
1613 }
1614 }
1615 } }
1616 if rlvl == 0 { r_ok = r_ok + 1; o = pj_cat(hb, o, "OK" as *u8) }
1617 if rlvl == 1 { r_ti = r_ti + 1; o = pj_cat(hb, o, "<span class='warn'>TIGHT</span>" as *u8) }
1618 if rlvl == 2 { r_la = r_la + 1; o = pj_cat(hb, o, "<span class='BLOCKED'>LATE</span>" as *u8) }
1619 if rlvl == 3 { r_ov = r_ov + 1; o = pj_cat(hb, o, "<span class='BLOCKED'>OVERDUE</span>" as *u8) }
1620 if rlvl == 4 { o = pj_cat(hb, o, "<span class='BLOCKED'>CYCLE</span>" as *u8) }
1621 if rlvl < 0 { o = pj_cat(hb, o, "-" as *u8) }
1622 o = pj_cat(hb, o, "</td><td>" as *u8)
1623 o = pj_catn(hb, o, efforts[i])
1624 o = pj_cat(hb, o, "</td><td>" as *u8)
1625 o = pj_cat(hb, o, titles[i] as *u8)
1626 o = pj_cat(hb, o, "</td></tr>" as *u8)
1627 }
1628 i = i + 1
1629 }
1630 if st[18] == 1 {
1631 o = pj_cat(hb, o, "</table></div><p class='meta'>risk: overdue=" as *u8)
1632 o = pj_catn(hb, o, r_ov)
1633 o = pj_cat(hb, o, " late=" as *u8)
1634 o = pj_catn(hb, o, r_la)
1635 o = pj_cat(hb, o, " tight=" as *u8)
1636 o = pj_catn(hb, o, r_ti)
1637 o = pj_cat(hb, o, " ok=" as *u8)
1638 o = pj_catn(hb, o, r_ok)
1639 o = pj_cat(hb, o, " (chain-slack vs WCONF " as *u8)
1640 o = pj_catn(hb, o, st[19])
1641 o = pj_cat(hb, o, " min/day, tight band " as *u8)
1642 o = pj_catn(hb, o, st[20])
1643 o = pj_cat(hb, o, " days)</p><h2>Workload</h2><div class='tablewrap'><table><tr><th>person</th><th>role</th><th>open min</th><th>cap min</th><th>over</th></tr>" as *u8)
1644 } else {
1645 o = pj_cat(hb, o, "</table></div><h2>Workload</h2><div class='tablewrap'><table><tr><th>person</th><th>role</th><th>open min</th><th>cap min</th><th>over</th></tr>" as *u8)
1646 }
1647 let mp: *i64 = st[9] as *i64
1648 let mr: *i64 = st[10] as *i64
1649 let mc: *i64 = st[11] as *i64
1650 var m: i64 = 0
1651 while m < st[8] {
1652 var sum: i64 = 0
1653 i = 0
1654 while i < st[0] {
1655 if pj_eq(owners[i] as *u8, mp[m] as *u8) == 1 {
1656 let sv2: i64 = pj_state(st, i)
1657 if sv2 != 2 { sum = sum + efforts[i] }
1658 }
1659 i = i + 1
1660 }
1661 o = pj_cat(hb, o, "<tr><td>" as *u8)
1662 o = pj_cat(hb, o, mp[m] as *u8)
1663 o = pj_cat(hb, o, "</td><td>" as *u8)
1664 o = pj_cat(hb, o, mr[m] as *u8)
1665 o = pj_cat(hb, o, "</td><td>" as *u8)
1666 o = pj_catn(hb, o, sum)
1667 o = pj_cat(hb, o, "</td><td>" as *u8)
1668 o = pj_cat(hb, o, mc[m] as *u8)
1669 o = pj_cat(hb, o, "</td><td>" as *u8)
1670 var overm: i64 = 0 - 1
1671 if pj_digits(mc[m] as *u8) == 1 {
1672 overm = 0
1673 if sum > pj_atoi(mc[m] as *u8) { overm = 1 }
1674 }
1675 if overm == 1 { o = pj_cat(hb, o, "<span class='BLOCKED'>over</span>" as *u8) }
1676 if overm == 0 { o = pj_cat(hb, o, "within" as *u8) }
1677 if overm < 0 { o = pj_cat(hb, o, "-" as *u8) }
1678 o = pj_cat(hb, o, "</td></tr>" as *u8)
1679 m = m + 1
1680 }
1681 o = pj_cat(hb, o, "</table></div>" as *u8)
1682 if st[15] == 1 {
1683 let efforts2: *i64 = st[4] as *i64
1684 var total: i64 = 0
1685 var done: i64 = 0
1686 i = 0
1687 while i < st[0] {
1688 total = total + efforts2[i]
1689 let sv3: i64 = pj_state(st, i)
1690 if sv3 == 2 { done = done + efforts2[i] }
1691 i = i + 1
1692 }
1693 if total > 0 {
1694 let scope: i64 = done * 1000 / total
1695 let ev: i64 = st[16] * scope / 1000
1696 o = pj_cat(hb, o, "<h2>Budget</h2><p>budget cents=" as *u8)
1697 o = pj_catn(hb, o, st[16])
1698 o = pj_cat(hb, o, " spent cents=" as *u8)
1699 o = pj_catn(hb, o, st[17])
1700 o = pj_cat(hb, o, " scope done permil=" as *u8)
1701 o = pj_catn(hb, o, scope)
1702 o = pj_cat(hb, o, " earned value cents=" as *u8)
1703 o = pj_catn(hb, o, ev)
1704 if st[17] > st[16] { o = pj_cat(hb, o, " <span class='BLOCKED'>OVER BUDGET</span>" as *u8) }
1705 o = pj_cat(hb, o, "</p>" as *u8)
1706 }
1707 }
1708 // ---- Schedule (latest-safe windows): bar = own-effort days ending at due; needs WCONF ----
1709 if st[18] == 1 { if st[0] > 0 {
1710 let gs: *i64 = sys_mmap(8 * 128) as *i64
1711 let gw: *i64 = sys_mmap(8 * 128) as *i64
1712 let gv: *i64 = sys_mmap(8 * 128) as *i64
1713 var minS: i64 = 0
1714 var maxD: i64 = 0
1715 var haveg: i64 = 0
1716 var gi: i64 = 0
1717 while gi < st[0] {
1718 gv[gi] = 0
1719 var visg: i64 = 1
1720 if own_only == 1 { if pj_eq(owners[gi] as *u8, person) == 0 { visg = 0 } }
1721 if visg == 1 {
1722 let gdd: i64 = pj_datedays(dues[gi] as *u8)
1723 if gdd >= 0 {
1724 var gdn: i64 = (efforts[gi] + st[19] - 1) / st[19]
1725 if gdn < 1 { gdn = 1 }
1726 let gst2: i64 = gdd - gdn
1727 gs[gi] = gst2
1728 gw[gi] = gdn
1729 gv[gi] = 1
1730 if haveg == 0 { minS = gst2; maxD = gdd; haveg = 1 } else {
1731 if gst2 < minS { minS = gst2 }
1732 if gdd > maxD { maxD = gdd }
1733 }
1734 }
1735 }
1736 gi = gi + 1
1737 }
1738 if haveg == 1 {
1739 var rng: i64 = maxD - minS
1740 if rng < 1 { rng = 1 }
1741 o = pj_cat(hb, o, "<h2>Schedule (latest-safe windows)</h2>" as *u8)
1742 gi = 0
1743 while gi < st[0] {
1744 if gv[gi] == 1 {
1745 let svg: i64 = pj_state(st, gi)
1746 let lpct: i64 = (gs[gi] - minS) * 100 / rng
1747 var wpct: i64 = gw[gi] * 100 / rng
1748 if wpct < 2 { wpct = 2 }
1749 if lpct + wpct > 100 { wpct = 100 - lpct }
1750 if wpct < 1 { wpct = 1 }
1751 o = pj_cat(hb, o, "<div class='gr'><span class='gl'>" as *u8)
1752 o = pj_cat(hb, o, tids[gi] as *u8)
1753 o = pj_cat(hb, o, "</span><div class='gwrap'><div class='gb " as *u8)
1754 if svg == 2 { o = pj_cat(hb, o, "gbD" as *u8) }
1755 if svg == 1 { o = pj_cat(hb, o, "gbB" as *u8) }
1756 if svg == 0 { o = pj_cat(hb, o, "gbR" as *u8) }
1757 o = pj_cat(hb, o, "' style='margin-left:" as *u8)
1758 o = pj_catn(hb, o, lpct)
1759 o = pj_cat(hb, o, "%;width:" as *u8)
1760 o = pj_catn(hb, o, wpct)
1761 o = pj_cat(hb, o, "%'></div></div></div>" as *u8)
1762 }
1763 gi = gi + 1
1764 }
1765 o = pj_cat(hb, o, "<p class='meta'>bars = own-effort window ending at due (WCONF " as *u8)
1766 o = pj_catn(hb, o, st[19])
1767 o = pj_cat(hb, o, " min/day) - blue ready, red blocked, green done</p>" as *u8)
1768 }
1769 } }
1770 // ---- Timesheet (logged minutes; contractor sees own logs only) ----
1771 if st[22] > 0 {
1772 let lp2: *i64 = st[24] as *i64
1773 let lm2: *i64 = st[25] as *i64
1774 var hdr2: i64 = 0
1775 let seen2: *i64 = sys_mmap(8 * 256) as *i64
1776 var ns2: i64 = 0
1777 var li: i64 = 0
1778 while li < st[22] {
1779 let lper: *u8 = lp2[li] as *u8
1780 var okp: i64 = 1
1781 if own_only == 1 { if pj_eq(lper, person) == 0 { okp = 0 } }
1782 if okp == 1 { if pj_find(seen2, ns2, lper) < 0 {
1783 if hdr2 == 0 {
1784 o = pj_cat(hb, o, "<h2>Timesheet</h2><div class='tablewrap'><table><tr><th>person</th><th>logged min</th></tr>" as *u8)
1785 hdr2 = 1
1786 }
1787 seen2[ns2] = lper as i64
1788 ns2 = ns2 + 1
1789 var lsum: i64 = 0
1790 var lk: i64 = 0
1791 while lk < st[22] {
1792 if pj_eq(lp2[lk] as *u8, lper) == 1 { lsum = lsum + lm2[lk] }
1793 lk = lk + 1
1794 }
1795 o = pj_cat(hb, o, "<tr><td>" as *u8)
1796 o = pj_cat(hb, o, lper)
1797 o = pj_cat(hb, o, "</td><td>" as *u8)
1798 o = pj_catn(hb, o, lsum)
1799 o = pj_cat(hb, o, "</td></tr>" as *u8)
1800 } }
1801 li = li + 1
1802 }
1803 if hdr2 == 1 { o = pj_cat(hb, o, "</table></div>" as *u8) }
1804 }
1805 // ---- Notes (task discussion; a note is visible only when its TASK is visible) ----
1806 if st[26] > 0 {
1807 let nt2: *i64 = st[27] as *i64
1808 let np2: *i64 = st[28] as *i64
1809 let nx2: *i64 = st[29] as *i64
1810 let nd2: *i64 = st[30] as *i64
1811 var hdr3: i64 = 0
1812 var ni: i64 = 0
1813 while ni < st[26] {
1814 var okn: i64 = 1
1815 if own_only == 1 {
1816 let ti2: i64 = pj_find(tids, st[0], nt2[ni] as *u8)
1817 if ti2 < 0 { okn = 0 } else { if pj_eq(owners[ti2] as *u8, person) == 0 { okn = 0 } }
1818 }
1819 if okn == 1 {
1820 if hdr3 == 0 { o = pj_cat(hb, o, "<h2>Notes</h2>" as *u8); hdr3 = 1 }
1821 o = pj_cat(hb, o, "<p class='meta'><b>" as *u8)
1822 o = pj_cat(hb, o, nt2[ni] as *u8)
1823 o = pj_cat(hb, o, "</b> " as *u8)
1824 o = pj_cat(hb, o, np2[ni] as *u8)
1825 o = pj_cat(hb, o, " " as *u8)
1826 o = pj_cat(hb, o, nd2[ni] as *u8)
1827 o = pj_cat(hb, o, " - " as *u8)
1828 o = pj_cat(hb, o, nx2[ni] as *u8)
1829 o = pj_cat(hb, o, "</p>" as *u8)
1830 }
1831 ni = ni + 1
1832 }
1833 }
1834 o = pj_foot(hb, o)
1835 return o
1836}
1837
1838// CLI wrapper: build + write to outfile (or - for stdout); loud on failure (the MCP/CLI surface)
1839func pj_html(prefix: *u8, pid: *u8, person: *u8, today: *u8, outpath: *u8) -> i64 {
1840 let hb: *u8 = sys_mmap(K_MAGIC_65536)
1841 let n: i64 = pj_html_build(prefix, pid, person, today, hb)
1842 if n == 0 - 3 { pj_p("PROJECT html bad path or today date -- fail loud\n" as *u8); return 1 }
1843 if n == 0 - 2 { pj_p("PROJECT-DENY -- not a member\n" as *u8); return 2 }
1844 if n < 0 { pj_p("PROJECT-NOTFOUND -- fail loud\n" as *u8); return 1 }
1845 var fd: i64 = 1
1846 var opened: i64 = 0
1847 var isdash: i64 = 0
1848 if outpath[0] == (45 as u8) { if outpath[1] == (0 as u8) { isdash = 1 } }
1849 if isdash == 0 {
1850 let f2: i64 = sys_openat_wr(outpath, 0x1a4)
1851 if f2 < 0 { pj_p("PROJECT html cannot open outfile -- fail loud\n" as *u8); return 1 }
1852 fd = f2
1853 opened = 1
1854 }
1855 sys_write(fd, hb, n)
1856 if opened == 1 { sys_close(fd) }
1857 return 0
1858}
1859
1860// epoch-days -> civil (Howard Hinnant); out = y,m,d
1861func pj_civil(z0: i64, out: *i64) -> i64 {
1862 var z: i64 = z0 + K_MAGIC_719468
1863 var era: i64 = z / K_MAGIC_146097
1864 if z < 0 { era = (z - K_MAGIC_146096) / K_MAGIC_146097 }
1865 let doe: i64 = z - era * K_MAGIC_146097
1866 let yoe: i64 = (doe - doe / K_MAGIC_1460 + doe / K_MAGIC_36524 - doe / K_MAGIC_146096) / 365
1867 let y0: i64 = yoe + era * 400
1868 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100)
1869 let mp: i64 = (5 * doy + 2) / 153
1870 let d: i64 = doy - (153 * mp + 2) / 5 + 1
1871 var m: i64 = mp + 3
1872 if mp >= 10 { m = mp - 9 }
1873 var y: i64 = y0
1874 if m <= 2 { y = y0 + 1 }
1875 out[0] = y
1876 out[1] = m
1877 out[2] = d
1878 return 0
1879}
1880
1881// epoch-days -> "YYYY-MM-DD" into out; returns length
1882func pj_fmtdate(days: i64, out: *u8) -> i64 {
1883 let ymd: *i64 = sys_mmap(32) as *i64
1884 pj_civil(days, ymd)
1885 var o: i64 = 0
1886 o = pj_catn(out, o, ymd[0])
1887 out[o] = 45 as u8
1888 o = o + 1
1889 if ymd[1] < 10 { out[o] = 48 as u8; o = o + 1 }
1890 o = pj_catn(out, o, ymd[1])
1891 out[o] = 45 as u8
1892 o = o + 1
1893 if ymd[2] < 10 { out[o] = 48 as u8; o = o + 1 }
1894 o = pj_catn(out, o, ymd[2])
1895 out[o] = 0 as u8
1896 return o
1897}
1898
1899// ---------------- TIMESHEET (logged minutes per person; out: 0=total 1=persons-with-logs) ----------------
1900func pj_cmd_timesheet(prefix: *u8, pid: *u8, out: *i64) -> i64 {
1901 out[0] = 0
1902 out[1] = 0
1903 if pj_pathok(prefix) == 0 { return pj_pathfail() }
1904 let st: *i64 = pj_st_new()
1905 if pj_gather(prefix, pid, st) != 0 { return 1 }
1906 let rep: *u8 = sys_mmap(K_MAGIC_2048)
1907 if st[12] == 0 {
1908 var o0: i64 = 0
1909 o0 = pj_cat(rep, o0, "PROJECT-NOTFOUND pid=" as *u8)
1910 o0 = pj_cat(rep, o0, pid)
1911 o0 = pj_cat(rep, o0, " -- fail loud\n" as *u8)
1912 sys_write(1, rep, o0)
1913 return 1
1914 }
1915 let lp: *i64 = st[24] as *i64
1916 let lm: *i64 = st[25] as *i64
1917 let seen: *i64 = sys_mmap(8 * 256) as *i64
1918 var nseen: i64 = 0
1919 var total: i64 = 0
1920 var i: i64 = 0
1921 while i < st[22] { total = total + lm[i]; i = i + 1 }
1922 i = 0
1923 while i < st[22] {
1924 let person: *u8 = lp[i] as *u8
1925 if pj_find(seen, nseen, person) < 0 {
1926 seen[nseen] = person as i64
1927 nseen = nseen + 1
1928 var sum: i64 = 0
1929 var k: i64 = 0
1930 while k < st[22] {
1931 if pj_eq(lp[k] as *u8, person) == 1 { sum = sum + lm[k] }
1932 k = k + 1
1933 }
1934 var t: i64 = 0
1935 t = pj_cat(rep, t, "PROJECT-TIME person=" as *u8)
1936 t = pj_cat(rep, t, person)
1937 t = pj_cat(rep, t, " logged_min=" as *u8)
1938 t = pj_catn(rep, t, sum)
1939 t = pj_cat(rep, t, "\n" as *u8)
1940 sys_write(1, rep, t)
1941 }
1942 i = i + 1
1943 }
1944 var z: i64 = 0
1945 z = pj_cat(rep, z, "PROJECT-TIME-END persons=" as *u8)
1946 z = pj_catn(rep, z, nseen)
1947 z = pj_cat(rep, z, " total_min=" as *u8)
1948 z = pj_catn(rep, z, total)
1949 z = pj_cat(rep, z, "\n" as *u8)
1950 sys_write(1, rep, z)
1951 out[0] = total
1952 out[1] = nseen
1953 return 0
1954}
1955
1956// ---------------- NOTES (task discussion; out: 0=count shown) ----------------
1957func pj_cmd_notes(prefix: *u8, pid: *u8, tid: *u8, out: *i64) -> i64 {
1958 out[0] = 0
1959 if pj_pathok(prefix) == 0 { return pj_pathfail() }
1960 let st: *i64 = pj_st_new()
1961 if pj_gather(prefix, pid, st) != 0 { return 1 }
1962 if st[12] == 0 { pj_p("PROJECT-NOTFOUND -- fail loud\n" as *u8); return 1 }
1963 let tids: *i64 = st[1] as *i64
1964 if pj_find(tids, st[0], tid) < 0 { pj_p("PROJECT notes: no such task -- fail loud\n" as *u8); return 1 }
1965 let rep: *u8 = sys_mmap(K_MAGIC_2048)
1966 let nt: *i64 = st[27] as *i64
1967 let np: *i64 = st[28] as *i64
1968 let nx: *i64 = st[29] as *i64
1969 let nd: *i64 = st[30] as *i64
1970 var shown: i64 = 0
1971 var i: i64 = 0
1972 while i < st[26] {
1973 if pj_eq(nt[i] as *u8, tid) == 1 {
1974 var t: i64 = 0
1975 t = pj_cat(rep, t, "PROJECT-NOTE tid=" as *u8)
1976 t = pj_cat(rep, t, tid)
1977 t = pj_cat(rep, t, " person=" as *u8)
1978 t = pj_cat(rep, t, np[i] as *u8)
1979 t = pj_cat(rep, t, " date=" as *u8)
1980 t = pj_cat(rep, t, nd[i] as *u8)
1981 t = pj_cat(rep, t, " text=" as *u8)
1982 t = pj_cat(rep, t, nx[i] as *u8)
1983 t = pj_cat(rep, t, "\n" as *u8)
1984 sys_write(1, rep, t)
1985 shown = shown + 1
1986 }
1987 i = i + 1
1988 }
1989 var z: i64 = 0
1990 z = pj_cat(rep, z, "PROJECT-NOTES-END tid=" as *u8)
1991 z = pj_cat(rep, z, tid)
1992 z = pj_cat(rep, z, " count=" as *u8)
1993 z = pj_catn(rep, z, shown)
1994 z = pj_cat(rep, z, "\n" as *u8)
1995 sys_write(1, rep, z)
1996 out[0] = shown
1997 return 0
1998}
1999
2000// ---------------- TEMPLATE (blueprint -> dated TASK/MILE file the planner loads) ----------------
2001// tpl rows: TPLTASK <tid> <owner> <offset-days> <effort> <imp> <deps|-> <title>
2002// TPLMILE <mid> <offset-days> <title>
2003func pj_cmd_template(argc: i64, argv: *i64) -> i64 {
2004 if argc < 6 { pj_p("PROJECT template needs <tplfile> <pid> <startdate> <outfile> -- fail loud\n" as *u8); return 1 }
2005 let tplf: *u8 = argv[2] as *u8
2006 let pid: *u8 = argv[3] as *u8
2007 let start: *u8 = argv[4] as *u8
2008 let outf: *u8 = argv[5] as *u8
2009 if pj_pathok(tplf) == 0 { return pj_pathfail() }
2010 if pj_pathok(outf) == 0 { return pj_pathfail() }
2011 let sd: i64 = pj_datedays(start)
2012 if sd < 0 { pj_p("PROJECT template bad start date -- fail loud\n" as *u8); return 1 }
2013 let szp: *i64 = sys_mmap(16) as *i64
2014 let b: *u8 = ss_readall(tplf, szp)
2015 let sz: i64 = szp[0]
2016 if sz <= 0 { pj_p("PROJECT template file MISSING/EMPTY -- fail loud\n" as *u8); return 1 }
2017 let ob: *u8 = sys_mmap(K_MAGIC_65536)
2018 var oo: i64 = 0
2019 let t0: *u8 = sys_mmap(128)
2020 let g1: *u8 = sys_mmap(128)
2021 let g2: *u8 = sys_mmap(128)
2022 let g3: *u8 = sys_mmap(128)
2023 let g4: *u8 = sys_mmap(128)
2024 let g5: *u8 = sys_mmap(128)
2025 let g6: *u8 = sys_mmap(256)
2026 let g7: *u8 = sys_mmap(256)
2027 let dbuf: *u8 = sys_mmap(32)
2028 var rows: i64 = 0
2029 var i: i64 = 0
2030 while i < sz {
2031 var e: i64 = i
2032 var go: i64 = 1
2033 while go == 1 {
2034 if e >= sz { go = 0 } else {
2035 if b[e] == (10 as u8) { go = 0 } else { e = e + 1 }
2036 }
2037 }
2038 var p: i64 = pj_tok(b, i, e, t0, 128)
2039 if t0[0] == (35 as u8) { t0[0] = 0 as u8 }
2040 if pj_eq(t0, "TPLTASK" as *u8) == 1 {
2041 p = pj_tok(b, p, e, g1, 128)
2042 p = pj_tok(b, p, e, g2, 128)
2043 p = pj_tok(b, p, e, g3, 128)
2044 p = pj_tok(b, p, e, g4, 128)
2045 p = pj_tok(b, p, e, g5, 128)
2046 p = pj_tok(b, p, e, g6, 256)
2047 p = pj_tok(b, p, e, g7, 256)
2048 if g1[0] == (0 as u8) { pj_p("PROJECT template TPLTASK missing tid -- fail loud\n" as *u8); return 1 }
2049 if g2[0] == (0 as u8) { pj_p("PROJECT template TPLTASK missing owner -- fail loud\n" as *u8); return 1 }
2050 if pj_digits(g3) == 0 { pj_p("PROJECT template TPLTASK offset must be digits -- fail loud\n" as *u8); return 1 }
2051 if pj_digits(g4) == 0 { pj_p("PROJECT template TPLTASK effort must be digits -- fail loud\n" as *u8); return 1 }
2052 if pj_digits(g5) == 0 { pj_p("PROJECT template TPLTASK imp must be digits -- fail loud\n" as *u8); return 1 }
2053 if g6[0] == (0 as u8) { pj_p("PROJECT template TPLTASK missing deps -- fail loud\n" as *u8); return 1 }
2054 if g7[0] == (0 as u8) { pj_p("PROJECT template TPLTASK missing title -- fail loud\n" as *u8); return 1 }
2055 pj_fmtdate(sd + pj_atoi(g3), dbuf)
2056 oo = pj_cat(ob, oo, "TASK " as *u8)
2057 oo = pj_cat(ob, oo, pid)
2058 ob[oo] = 32 as u8
2059 oo = oo + 1
2060 oo = pj_cat(ob, oo, g1)
2061 ob[oo] = 32 as u8
2062 oo = oo + 1
2063 oo = pj_cat(ob, oo, g2)
2064 ob[oo] = 32 as u8
2065 oo = oo + 1
2066 oo = pj_cat(ob, oo, dbuf)
2067 ob[oo] = 32 as u8
2068 oo = oo + 1
2069 oo = pj_cat(ob, oo, g4)
2070 ob[oo] = 32 as u8
2071 oo = oo + 1
2072 oo = pj_cat(ob, oo, g5)
2073 ob[oo] = 32 as u8
2074 oo = oo + 1
2075 oo = pj_cat(ob, oo, g6)
2076 ob[oo] = 32 as u8
2077 oo = oo + 1
2078 oo = pj_cat(ob, oo, g7)
2079 oo = pj_cat(ob, oo, "\n" as *u8)
2080 rows = rows + 1
2081 }
2082 if pj_eq(t0, "TPLMILE" as *u8) == 1 {
2083 p = pj_tok(b, p, e, g1, 128)
2084 p = pj_tok(b, p, e, g3, 128)
2085 p = pj_tok(b, p, e, g7, 256)
2086 if g1[0] == (0 as u8) { pj_p("PROJECT template TPLMILE missing mid -- fail loud\n" as *u8); return 1 }
2087 if pj_digits(g3) == 0 { pj_p("PROJECT template TPLMILE offset must be digits -- fail loud\n" as *u8); return 1 }
2088 if g7[0] == (0 as u8) { pj_p("PROJECT template TPLMILE missing title -- fail loud\n" as *u8); return 1 }
2089 pj_fmtdate(sd + pj_atoi(g3), dbuf)
2090 oo = pj_cat(ob, oo, "MILE " as *u8)
2091 oo = pj_cat(ob, oo, pid)
2092 ob[oo] = 32 as u8
2093 oo = oo + 1
2094 oo = pj_cat(ob, oo, g1)
2095 ob[oo] = 32 as u8
2096 oo = oo + 1
2097 oo = pj_cat(ob, oo, dbuf)
2098 ob[oo] = 32 as u8
2099 oo = oo + 1
2100 oo = pj_cat(ob, oo, g7)
2101 oo = pj_cat(ob, oo, "\n" as *u8)
2102 rows = rows + 1
2103 }
2104 i = e + 1
2105 }
2106 if rows == 0 { pj_p("PROJECT template: no TPLTASK/TPLMILE rows -- fail loud\n" as *u8); return 1 }
2107 if pj_wfile(outf, ob) != 0 { pj_p("PROJECT template cannot write outfile -- fail loud\n" as *u8); return 1 }
2108 let rep: *u8 = sys_mmap(256)
2109 var z: i64 = 0
2110 z = pj_cat(rep, z, "PROJECT-TEMPLATE rows=" as *u8)
2111 z = pj_catn(rep, z, rows)
2112 z = pj_cat(rep, z, " start=" as *u8)
2113 z = pj_cat(rep, z, start)
2114 z = pj_cat(rep, z, " -- load the outfile with the planner to instantiate\n" as *u8)
2115 sys_write(1, rep, z)
2116 return 0
2117}
2118
2119// ---------------- SNAPSHOT (write a BASELINE = each task's current due+effort, dated) ----------------
2120func pj_cmd_snapshot(prefix: *u8, pid: *u8, snapdate: *u8, outfile: *u8) -> i64 {
2121 if pj_pathok(prefix) == 0 { return pj_pathfail() }
2122 if pj_pathok(outfile) == 0 { return pj_pathfail() }
2123 if pj_datedays(snapdate) < 0 { pj_p("PROJECT snapshot bad date -- fail loud\n" as *u8); return 1 }
2124 let st: *i64 = pj_st_new()
2125 if pj_gather(prefix, pid, st) != 0 { return 1 }
2126 if st[12] == 0 { pj_p("PROJECT-NOTFOUND -- fail loud\n" as *u8); return 1 }
2127 let tids: *i64 = st[1] as *i64
2128 let dues: *i64 = st[21] as *i64
2129 let efforts: *i64 = st[4] as *i64
2130 let ob: *u8 = sys_mmap(K_MAGIC_65536)
2131 var o: i64 = 0
2132 var i: i64 = 0
2133 while i < st[0] {
2134 o = pj_cat(ob, o, "BASE " as *u8)
2135 o = pj_cat(ob, o, pid)
2136 ob[o] = 32 as u8
2137 o = o + 1
2138 o = pj_cat(ob, o, tids[i] as *u8)
2139 ob[o] = 32 as u8
2140 o = o + 1
2141 o = pj_cat(ob, o, dues[i] as *u8)
2142 ob[o] = 32 as u8
2143 o = o + 1
2144 o = pj_catn(ob, o, efforts[i])
2145 ob[o] = 32 as u8
2146 o = o + 1
2147 o = pj_cat(ob, o, snapdate)
2148 o = pj_cat(ob, o, "\n" as *u8)
2149 i = i + 1
2150 }
2151 ob[o] = 0 as u8
2152 if pj_wfile(outfile, ob) != 0 { pj_p("PROJECT snapshot cannot write -- fail loud\n" as *u8); return 1 }
2153 let rep: *u8 = sys_mmap(256)
2154 var z: i64 = 0
2155 z = pj_cat(rep, z, "PROJECT-SNAPSHOT tasks=" as *u8)
2156 z = pj_catn(rep, z, st[0])
2157 z = pj_cat(rep, z, " date=" as *u8)
2158 z = pj_cat(rep, z, snapdate)
2159 z = pj_cat(rep, z, " -- load the outfile to record the baseline (BASE records, additive)\n" as *u8)
2160 sys_write(1, rep, z)
2161 return 0
2162}
2163
2164// ---------------- BASELINE VARIANCE (current plan vs the recorded baseline) ----------------
2165// out: 0=tasks-with-baseline 1=total-slip-days 2=tasks-slipped 3=tasks-no-baseline
2166func pj_cmd_baseline(prefix: *u8, pid: *u8, out: *i64) -> i64 {
2167 out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 0
2168 if pj_pathok(prefix) == 0 { return pj_pathfail() }
2169 let st: *i64 = pj_st_new()
2170 if pj_gather(prefix, pid, st) != 0 { return 1 }
2171 if st[12] == 0 { pj_p("PROJECT-NOTFOUND -- fail loud\n" as *u8); return 1 }
2172 if st[36] == 0 { pj_p("PROJECT baseline: none recorded (run snapshot first) -- fail loud\n" as *u8); return 1 }
2173 let rep: *u8 = sys_mmap(K_MAGIC_2048)
2174 let tids: *i64 = st[1] as *i64
2175 let dues: *i64 = st[21] as *i64
2176 let efforts: *i64 = st[4] as *i64
2177 let bt: *i64 = st[37] as *i64
2178 let bd: *i64 = st[38] as *i64
2179 let be: *i64 = st[39] as *i64
2180 var i: i64 = 0
2181 while i < st[0] {
2182 let bi: i64 = pj_find(bt, st[36], tids[i] as *u8)
2183 if bi < 0 {
2184 out[3] = out[3] + 1
2185 } else {
2186 let curdue: i64 = pj_datedays(dues[i] as *u8)
2187 let slip: i64 = curdue - bd[bi]
2188 let edelta: i64 = efforts[i] - be[bi]
2189 out[0] = out[0] + 1
2190 out[1] = out[1] + slip
2191 if slip > 0 { out[2] = out[2] + 1 }
2192 var t: i64 = 0
2193 t = pj_cat(rep, t, "PROJECT-BASELINE tid=" as *u8)
2194 t = pj_cat(rep, t, tids[i] as *u8)
2195 t = pj_cat(rep, t, " due_slip_days=" as *u8)
2196 t = pj_catn(rep, t, slip)
2197 t = pj_cat(rep, t, " effort_delta_min=" as *u8)
2198 t = pj_catn(rep, t, edelta)
2199 if slip > 0 { t = pj_cat(rep, t, " SLIPPED" as *u8) }
2200 t = pj_cat(rep, t, "\n" as *u8)
2201 sys_write(1, rep, t)
2202 }
2203 i = i + 1
2204 }
2205 var z: i64 = 0
2206 z = pj_cat(rep, z, "PROJECT-BASELINE-END baselined=" as *u8)
2207 z = pj_catn(rep, z, out[0])
2208 z = pj_cat(rep, z, " total_slip_days=" as *u8)
2209 z = pj_catn(rep, z, out[1])
2210 z = pj_cat(rep, z, " slipped=" as *u8)
2211 z = pj_catn(rep, z, out[2])
2212 z = pj_cat(rep, z, " no_baseline=" as *u8)
2213 z = pj_catn(rep, z, out[3])
2214 z = pj_cat(rep, z, "\n" as *u8)
2215 sys_write(1, rep, z)
2216 return 0
2217}
2218
2219// ---------------- BURNDOWN (remaining effort over time from DONE + effort records) ----------------
2220// out: 0=total_effort 1=done_effort 2=remaining 3=done_points
2221func pj_cmd_burndown(prefix: *u8, pid: *u8, out: *i64) -> i64 {
2222 out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 0
2223 if pj_pathok(prefix) == 0 { return pj_pathfail() }
2224 let st: *i64 = pj_st_new()
2225 if pj_gather(prefix, pid, st) != 0 { return 1 }
2226 if st[12] == 0 { pj_p("PROJECT-NOTFOUND -- fail loud\n" as *u8); return 1 }
2227 let rep: *u8 = sys_mmap(K_MAGIC_2048)
2228 let tids: *i64 = st[1] as *i64
2229 let efforts: *i64 = st[4] as *i64
2230 var total: i64 = 0
2231 var i: i64 = 0
2232 while i < st[0] { total = total + efforts[i]; i = i + 1 }
2233 // sort done entries by date (insertion sort on parallel dtids/ddates copies)
2234 let dt: *i64 = st[7] as *i64
2235 let dd: *i64 = st[35] as *i64
2236 let sidx: *i64 = sys_mmap(8 * 260) as *i64
2237 var nd: i64 = st[6]
2238 i = 0
2239 while i < nd { sidx[i] = i; i = i + 1 }
2240 i = 1
2241 while i < nd {
2242 let key: i64 = sidx[i]
2243 var j: i64 = i - 1
2244 var go: i64 = 1
2245 while go == 1 {
2246 if j < 0 { go = 0 } else {
2247 if dd[sidx[j]] > dd[key] { sidx[j + 1] = sidx[j]; j = j - 1 } else { go = 0 }
2248 }
2249 }
2250 sidx[j + 1] = key
2251 i = i + 1
2252 }
2253 var remaining: i64 = total
2254 var done_pts: i64 = 0
2255 i = 0
2256 while i < nd {
2257 let de: i64 = sidx[i]
2258 let ti: i64 = pj_find(tids, st[0], dt[de] as *u8)
2259 var eff: i64 = 0
2260 if ti >= 0 { eff = efforts[ti] }
2261 remaining = remaining - eff
2262 done_pts = done_pts + eff
2263 let ymd: *i64 = sys_mmap(32) as *i64
2264 pj_civil(dd[de], ymd)
2265 var t: i64 = 0
2266 t = pj_cat(rep, t, "PROJECT-BURN date=" as *u8)
2267 t = pj_catn(rep, t, ymd[0]); rep[t] = 45 as u8; t = t + 1
2268 if ymd[1] < 10 { rep[t] = 48 as u8; t = t + 1 }
2269 t = pj_catn(rep, t, ymd[1]); rep[t] = 45 as u8; t = t + 1
2270 if ymd[2] < 10 { rep[t] = 48 as u8; t = t + 1 }
2271 t = pj_catn(rep, t, ymd[2])
2272 t = pj_cat(rep, t, " tid=" as *u8)
2273 t = pj_cat(rep, t, dt[de] as *u8)
2274 t = pj_cat(rep, t, " burned=" as *u8)
2275 t = pj_catn(rep, t, eff)
2276 t = pj_cat(rep, t, " remaining_min=" as *u8)
2277 t = pj_catn(rep, t, remaining)
2278 t = pj_cat(rep, t, "\n" as *u8)
2279 sys_write(1, rep, t)
2280 i = i + 1
2281 }
2282 var z: i64 = 0
2283 z = pj_cat(rep, z, "PROJECT-BURN-END total_min=" as *u8)
2284 z = pj_catn(rep, z, total)
2285 z = pj_cat(rep, z, " done_min=" as *u8)
2286 z = pj_catn(rep, z, done_pts)
2287 z = pj_cat(rep, z, " remaining_min=" as *u8)
2288 z = pj_catn(rep, z, remaining)
2289 z = pj_cat(rep, z, " done_points=" as *u8)
2290 z = pj_catn(rep, z, st[6])
2291 z = pj_cat(rep, z, "\n" as *u8)
2292 sys_write(1, rep, z)
2293 out[0] = total
2294 out[1] = done_pts
2295 out[2] = remaining
2296 out[3] = st[6]
2297 return 0
2298}
2299
2300// ---------------- WORKLOAD ----------------
2301func pj_cmd_workload(prefix: *u8, pid: *u8) -> i64 {
2302 if pj_pathok(prefix) == 0 { return pj_pathfail() }
2303 let st: *i64 = pj_st_new()
2304 if pj_gather(prefix, pid, st) != 0 { return 1 }
2305 let rep: *u8 = sys_mmap(K_MAGIC_2048)
2306 if st[12] == 0 {
2307 var o0: i64 = 0
2308 o0 = pj_cat(rep, o0, "PROJECT-NOTFOUND pid=" as *u8)
2309 o0 = pj_cat(rep, o0, pid)
2310 o0 = pj_cat(rep, o0, " -- fail loud\n" as *u8)
2311 sys_write(1, rep, o0)
2312 return 1
2313 }
2314 let mp: *i64 = st[9] as *i64
2315 let mr: *i64 = st[10] as *i64
2316 let mc: *i64 = st[11] as *i64
2317 let owners: *i64 = st[2] as *i64
2318 let efforts: *i64 = st[4] as *i64
2319 var o: i64 = 0
2320 o = pj_cat(rep, o, "PROJECT-WORKLOAD pid=" as *u8)
2321 o = pj_cat(rep, o, pid)
2322 o = pj_cat(rep, o, " members=" as *u8)
2323 o = pj_catn(rep, o, st[8])
2324 o = pj_cat(rep, o, " tasks=" as *u8)
2325 o = pj_catn(rep, o, st[0])
2326 o = pj_cat(rep, o, "\n" as *u8)
2327 sys_write(1, rep, o)
2328 var m: i64 = 0
2329 while m < st[8] {
2330 let person: *u8 = mp[m] as *u8
2331 var sum: i64 = 0
2332 var i: i64 = 0
2333 while i < st[0] {
2334 if pj_eq(owners[i] as *u8, person) == 1 {
2335 let sv: i64 = pj_state(st, i)
2336 if sv != 2 { sum = sum + efforts[i] }
2337 }
2338 i = i + 1
2339 }
2340 let cap: *u8 = mc[m] as *u8
2341 var over: i64 = 0 - 1
2342 if pj_digits(cap) == 1 {
2343 over = 0
2344 if sum > pj_atoi(cap) { over = 1 }
2345 }
2346 var t: i64 = 0
2347 t = pj_cat(rep, t, "PROJECT-LOAD person=" as *u8)
2348 t = pj_cat(rep, t, person)
2349 t = pj_cat(rep, t, " role=" as *u8)
2350 t = pj_cat(rep, t, mr[m] as *u8)
2351 t = pj_cat(rep, t, " open_min=" as *u8)
2352 t = pj_catn(rep, t, sum)
2353 t = pj_cat(rep, t, " cap_min=" as *u8)
2354 t = pj_cat(rep, t, cap)
2355 t = pj_cat(rep, t, " over=" as *u8)
2356 if over == 1 { t = pj_cat(rep, t, "1" as *u8) }
2357 if over == 0 { t = pj_cat(rep, t, "0" as *u8) }
2358 if over < 0 { t = pj_cat(rep, t, "-" as *u8) }
2359 t = pj_cat(rep, t, "\n" as *u8)
2360 sys_write(1, rep, t)
2361 m = m + 1
2362 }
2363 // orphan owners: tasks assigned to someone with NO membership record (loud visibility)
2364 var i2: i64 = 0
2365 while i2 < st[0] {
2366 let ow: *u8 = owners[i2] as *u8
2367 var ismem: i64 = 0
2368 if pj_find(mp, st[8], ow) >= 0 { ismem = 1 }
2369 if st[12] == 1 { if pj_eq(st[13] as *u8, ow) == 1 { ismem = 1 } }
2370 if ismem == 0 {
2371 var t2: i64 = 0
2372 t2 = pj_cat(rep, t2, "PROJECT-LOAD-ORPHAN owner=" as *u8)
2373 t2 = pj_cat(rep, t2, ow)
2374 t2 = pj_cat(rep, t2, " -- task owner has no MEMBER record\n" as *u8)
2375 sys_write(1, rep, t2)
2376 }
2377 i2 = i2 + 1
2378 }
2379 return 0
2380}
2381
2382// ---------------- SELFTEST (the gate; forks the REAL blessed planner as the tsk:/don: writer) ----------------
2383func pj_run(elf: *u8, args: *i64, outpath: *u8) -> i64 {
2384 let pid: i64 = sys_fork()
2385 if pid == 0 {
2386 if (outpath as i64) != 0 {
2387 let ofd: i64 = sys_openat_wr(outpath, 0x1a4)
2388 if ofd >= 0 { sys_dup3(ofd, 1, 0); sys_dup3(ofd, 2, 0) }
2389 }
2390 let argv: *i64 = sys_mmap(128) as *i64
2391 argv[0] = elf as i64
2392 var i: i64 = 0
2393 var go: i64 = 1
2394 while go == 1 {
2395 if args[i] == 0 { go = 0 } else {
2396 argv[i + 1] = args[i]
2397 i = i + 1
2398 }
2399 }
2400 argv[i + 1] = 0
2401 let envp: *i64 = sys_mmap(16) as *i64
2402 envp[0] = 0
2403 sys_execve(elf, argv, envp)
2404 sys_exit(127)
2405 }
2406 let stp: *i64 = sys_mmap(16) as *i64
2407 sys_wait4(pid, stp, 0)
2408 let sig: i64 = stp[0] & 0x7f
2409 if sig != 0 { return 128 + sig }
2410 return (stp[0] >> 8) & 0xff
2411}
2412
2413func pj_check(idx: i64, ok: i64, name: *u8) -> i64 {
2414 let rep: *u8 = sys_mmap(256)
2415 var o: i64 = 0
2416 o = pj_cat(rep, o, "PJ-GATE T" as *u8)
2417 o = pj_catn(rep, o, idx)
2418 o = pj_cat(rep, o, " " as *u8)
2419 o = pj_cat(rep, o, name)
2420 if ok == 1 { o = pj_cat(rep, o, " PASS\n" as *u8) } else { o = pj_cat(rep, o, " FAIL\n" as *u8) }
2421 sys_write(1, rep, o)
2422 return ok
2423}
2424
2425func pj_selftest() -> i64 {
2426 let uniq: i64 = sys_now_us()
2427 let planfile: *u8 = sys_mmap(256)
2428 var o: i64 = 0
2429 o = pj_cat(planfile, o, "/tmp/pjt_" as *u8)
2430 o = pj_catn(planfile, o, uniq)
2431 o = pj_cat(planfile, o, "_plan.txt" as *u8)
2432 planfile[o] = 0 as u8
2433 let projfile: *u8 = sys_mmap(256)
2434 o = 0
2435 o = pj_cat(projfile, o, "/tmp/pjt_" as *u8)
2436 o = pj_catn(projfile, o, uniq)
2437 o = pj_cat(projfile, o, "_proj.txt" as *u8)
2438 projfile[o] = 0 as u8
2439 let badfile: *u8 = sys_mmap(256)
2440 o = 0
2441 o = pj_cat(badfile, o, "/tmp/pjt_" as *u8)
2442 o = pj_catn(badfile, o, uniq)
2443 o = pj_cat(badfile, o, "_bad.txt" as *u8)
2444 badfile[o] = 0 as u8
2445 let prefix: *u8 = sys_mmap(256)
2446 o = 0
2447 o = pj_cat(prefix, o, "/tmp/pjt_" as *u8)
2448 o = pj_catn(prefix, o, uniq)
2449 o = pj_cat(prefix, o, "_st-" as *u8)
2450 prefix[o] = 0 as u8
2451 let outf: *u8 = sys_mmap(256)
2452 o = 0
2453 o = pj_cat(outf, o, "/tmp/pjt_" as *u8)
2454 o = pj_catn(outf, o, uniq)
2455 o = pj_cat(outf, o, "_run.out" as *u8)
2456 outf[o] = 0 as u8
2457
2458 pj_wfile(planfile, "TASK demo t1 alice 2026-07-10 60 3 - setup\nTASK demo t2 bob 2026-07-12 120 4 t1 build\nTASK demo t3 carol 2026-07-14 90 5 t2 install\nDONE demo t1 2026-07-09\n" as *u8)
2459 pj_wfile(projfile, "PROJ demo elder website-build\nMEMBER demo alice team -\nMEMBER demo bob team -\nMEMBER demo carol contractor 60\n" as *u8)
2460 pj_wfile(badfile, "MEMBER demo dave boss -\n" as *u8)
2461
2462 var pass: i64 = 0
2463 // T1 the INDEPENDENT planner organ writes tsk:/don: records into the shared store
2464 let a1: *i64 = sys_mmap(8 * 8) as *i64
2465 a1[0] = "load" as *u8 as i64
2466 a1[1] = planfile as i64
2467 a1[2] = prefix as i64
2468 a1[3] = "7001" as *u8 as i64
2469 a1[4] = 0
2470 let rc1: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a1, outf)
2471 var ok: i64 = 0
2472 if rc1 == 0 { ok = 1 }
2473 pass = pass + pj_check(1, ok, "planner-writes-store" as *u8)
2474 // T2 our loader adds prj:/mem: beside them
2475 let av: *i64 = sys_mmap(8 * 8) as *i64
2476 av[0] = "pj" as *u8 as i64
2477 av[1] = "load" as *u8 as i64
2478 av[2] = projfile as i64
2479 av[3] = prefix as i64
2480 av[4] = "7002" as *u8 as i64
2481 let rc2: i64 = pj_cmd_load(5, av)
2482 ok = 0
2483 if rc2 == 0 { ok = 1 }
2484 pass = pass + pj_check(2, ok, "project-load" as *u8)
2485 // T3 states: t1 DONE, t2 READY (dep done), t3 BLOCKED (dep open)
2486 ok = 0
2487 let s1: i64 = pj_task_state(prefix, "demo" as *u8, "t1" as *u8)
2488 let s2: i64 = pj_task_state(prefix, "demo" as *u8, "t2" as *u8)
2489 let s3: i64 = pj_task_state(prefix, "demo" as *u8, "t3" as *u8)
2490 if s1 == 2 { if s2 == 0 { if s3 == 1 { ok = 1 } } }
2491 pass = pass + pj_check(3, ok, "dep-states" as *u8)
2492 // T4 contractor sees ONLY own task
2493 let out4: *i64 = sys_mmap(32) as *i64
2494 let rc4: i64 = pj_view(prefix, "demo" as *u8, "carol" as *u8, out4)
2495 ok = 0
2496 if rc4 == 0 { if out4[0] == 3 { if out4[1] == 1 { ok = 1 } } }
2497 pass = pass + pj_check(4, ok, "contractor-scope-own" as *u8)
2498 // T5 NEG: carol must NOT see t1; must see t3
2499 ok = 0
2500 let v1: i64 = pj_visible(prefix, "demo" as *u8, "carol" as *u8, "t1" as *u8)
2501 let v3: i64 = pj_visible(prefix, "demo" as *u8, "carol" as *u8, "t3" as *u8)
2502 if v1 == 0 { if v3 == 1 { ok = 1 } }
2503 pass = pass + pj_check(5, ok, "neg-contractor-hidden" as *u8)
2504 // T6 NEG: non-member denied fail-closed
2505 let out6: *i64 = sys_mmap(32) as *i64
2506 let rc6: i64 = pj_view(prefix, "demo" as *u8, "mallory" as *u8, out6)
2507 ok = 0
2508 if rc6 == 2 { ok = 1 }
2509 pass = pass + pj_check(6, ok, "neg-nonmember-deny" as *u8)
2510 // T7 NEG: unknown project fails loud
2511 let out7: *i64 = sys_mmap(32) as *i64
2512 let rc7: i64 = pj_view(prefix, "ghost" as *u8, "carol" as *u8, out7)
2513 ok = 0
2514 if rc7 == 1 { ok = 1 }
2515 pass = pass + pj_check(7, ok, "neg-unknown-project" as *u8)
2516 // T8 workload sums + data-driven over-capacity
2517 ok = 0
2518 let w1: i64 = pj_open_min(prefix, "demo" as *u8, "carol" as *u8)
2519 let w2: i64 = pj_open_min(prefix, "demo" as *u8, "alice" as *u8)
2520 let w3: i64 = pj_open_min(prefix, "demo" as *u8, "bob" as *u8)
2521 let ov1: i64 = pj_over(prefix, "demo" as *u8, "carol" as *u8)
2522 let ov2: i64 = pj_over(prefix, "demo" as *u8, "bob" as *u8)
2523 if w1 == 90 { if w2 == 0 { if w3 == 120 { if ov1 == 1 { if ov2 == 0 - 1 { ok = 1 } } } } }
2524 pass = pass + pj_check(8, ok, "workload-capacity" as *u8)
2525 // T9 NEG: malformed role refuses load
2526 let av9: *i64 = sys_mmap(8 * 8) as *i64
2527 av9[0] = "pj" as *u8 as i64
2528 av9[1] = "load" as *u8 as i64
2529 av9[2] = badfile as i64
2530 av9[3] = prefix as i64
2531 av9[4] = "7003" as *u8 as i64
2532 let rc9: i64 = pj_cmd_load(5, av9)
2533 ok = 0
2534 if rc9 != 0 { ok = 1 }
2535 pass = pass + pj_check(9, ok, "neg-bad-role-refused" as *u8)
2536 // T10 idempotent re-load: no duplicate members
2537 let av10: *i64 = sys_mmap(8 * 8) as *i64
2538 av10[0] = "pj" as *u8 as i64
2539 av10[1] = "load" as *u8 as i64
2540 av10[2] = projfile as i64
2541 av10[3] = prefix as i64
2542 av10[4] = "7004" as *u8 as i64
2543 let rc10: i64 = pj_cmd_load(5, av10)
2544 let st10: *i64 = pj_st_new()
2545 pj_gather(prefix, "demo" as *u8, st10)
2546 ok = 0
2547 if rc10 == 0 { if st10[8] == 3 { if st10[0] == 3 { ok = 1 } } }
2548 pass = pass + pj_check(10, ok, "idempotent-reload" as *u8)
2549 // T11 owner sees all
2550 let out11: *i64 = sys_mmap(32) as *i64
2551 let rc11: i64 = pj_view(prefix, "demo" as *u8, "elder" as *u8, out11)
2552 ok = 0
2553 if rc11 == 0 { if out11[1] == 3 { ok = 1 } }
2554 pass = pass + pj_check(11, ok, "owner-sees-all" as *u8)
2555 // T12 NEG: path guard refuses out-of-namespace and dot-dot stores
2556 let av12: *i64 = sys_mmap(8 * 8) as *i64
2557 av12[0] = "pj" as *u8 as i64
2558 av12[1] = "load" as *u8 as i64
2559 av12[2] = projfile as i64
2560 av12[3] = "knowledge/evil-" as *u8 as i64
2561 av12[4] = "7005" as *u8 as i64
2562 let rc12a: i64 = pj_cmd_load(5, av12)
2563 let av12b: *i64 = sys_mmap(8 * 8) as *i64
2564 av12b[0] = "pj" as *u8 as i64
2565 av12b[1] = "load" as *u8 as i64
2566 av12b[2] = "/tmp/../etc/passwd" as *u8 as i64
2567 av12b[3] = prefix as i64
2568 av12b[4] = "7006" as *u8 as i64
2569 let rc12b: i64 = pj_cmd_load(5, av12b)
2570 ok = 0
2571 if rc12a != 0 { if rc12b != 0 { ok = 1 } }
2572 pass = pass + pj_check(12, ok, "neg-path-guard" as *u8)
2573
2574 // ---- fence project fixtures (level / risk / evm / html) ----
2575 let plan2: *u8 = sys_mmap(256)
2576 o = 0
2577 o = pj_cat(plan2, o, "/tmp/pjt_" as *u8)
2578 o = pj_catn(plan2, o, uniq)
2579 o = pj_cat(plan2, o, "_plan2.txt" as *u8)
2580 plan2[o] = 0 as u8
2581 let proj2: *u8 = sys_mmap(256)
2582 o = 0
2583 o = pj_cat(proj2, o, "/tmp/pjt_" as *u8)
2584 o = pj_catn(proj2, o, uniq)
2585 o = pj_cat(proj2, o, "_proj2.txt" as *u8)
2586 proj2[o] = 0 as u8
2587 let prefix2: *u8 = sys_mmap(256)
2588 o = 0
2589 o = pj_cat(prefix2, o, "/tmp/pjt_" as *u8)
2590 o = pj_catn(prefix2, o, uniq)
2591 o = pj_cat(prefix2, o, "_st2-" as *u8)
2592 prefix2[o] = 0 as u8
2593 let htmlfile: *u8 = sys_mmap(256)
2594 o = 0
2595 o = pj_cat(htmlfile, o, "/tmp/pjt_" as *u8)
2596 o = pj_catn(htmlfile, o, uniq)
2597 o = pj_cat(htmlfile, o, "_page.html" as *u8)
2598 htmlfile[o] = 0 as u8
2599 pj_wfile(plan2, "TASK fence f0 amber 2026-07-01 30 1 - permits\nTASK fence f1 amber 2026-07-20 200 3 - posts\nTASK fence f2 bo 2026-07-18 100 4 - rails\nTASK fence f3 bo 2026-07-19 80 2 f2 gates\nTASK fence f4 cade 2026-07-21 60 3 - paint\nTASK fence f5 cade 2026-08-15 60 2 - cleanup\nDONE fence f4 2026-07-16\n" as *u8)
2600 pj_wfile(proj2, "PROJ fence elder fence-build\nMEMBER fence amber team 240\nMEMBER fence bo contractor 120\nMEMBER fence cade team 480\nBUDGET fence 100000\nWCONF fence 120 1\nCOST fence f2 30000 2026-07-15\nCOST fence f4 10000 2026-07-16\n" as *u8)
2601 let a2: *i64 = sys_mmap(8 * 8) as *i64
2602 a2[0] = "load" as *u8 as i64
2603 a2[1] = plan2 as i64
2604 a2[2] = prefix2 as i64
2605 a2[3] = "7101" as *u8 as i64
2606 a2[4] = 0
2607 let rcf1: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a2, outf)
2608 let avf: *i64 = sys_mmap(8 * 8) as *i64
2609 avf[0] = "pj" as *u8 as i64
2610 avf[1] = "load" as *u8 as i64
2611 avf[2] = proj2 as i64
2612 avf[3] = prefix2 as i64
2613 avf[4] = "7102" as *u8 as i64
2614 let rcf2: i64 = pj_cmd_load(5, avf)
2615 if rcf1 != 0 { pj_p("PJ-GATE fence planner load FAILED\n" as *u8) }
2616 if rcf2 != 0 { pj_p("PJ-GATE fence project load FAILED\n" as *u8) }
2617 // time logs + task notes into the same fence store (T19-24 substrate)
2618 let logsf: *u8 = sys_mmap(256)
2619 o = 0
2620 o = pj_cat(logsf, o, "/tmp/pjt_" as *u8)
2621 o = pj_catn(logsf, o, uniq)
2622 o = pj_cat(logsf, o, "_logs.txt" as *u8)
2623 logsf[o] = 0 as u8
2624 pj_wfile(logsf, "LOG fence f2 bo 2026-07-16 60\nLOG fence f2 bo 2026-07-17 30\nLOG fence f1 amber 2026-07-16 45\nNOTE fence f2 bo 2026-07-16 rails cut and primed\nNOTE fence f1 amber 2026-07-17 posts delivered late\n" as *u8)
2625 let avl: *i64 = sys_mmap(8 * 8) as *i64
2626 avl[0] = "pj" as *u8 as i64
2627 avl[1] = "load" as *u8 as i64
2628 avl[2] = logsf as i64
2629 avl[3] = prefix2 as i64
2630 avl[4] = "7103" as *u8 as i64
2631 let rcl: i64 = pj_cmd_load(5, avl)
2632 if rcl != 0 { pj_p("PJ-GATE logs load FAILED\n" as *u8) }
2633 // T13 leveling KAT: bo (cap 120, open 180) moves READY f2 (100) to cade (headroom 420) -> balanced
2634 let out13: *i64 = sys_mmap(32) as *i64
2635 let rc13: i64 = pj_cmd_level(prefix2, "fence" as *u8, out13, "-" as *u8)
2636 ok = 0
2637 if rc13 == 0 { if out13[0] == 1 { if out13[1] == 0 { ok = 1 } } }
2638 pass = pass + pj_check(13, ok, "level-move-kat" as *u8)
2639 // T14 NEG: demo carol is over-cap but her only task is BLOCKED -> no move, still over (named STUCK)
2640 let out14: *i64 = sys_mmap(32) as *i64
2641 let rc14: i64 = pj_cmd_level(prefix, "demo" as *u8, out14, "-" as *u8)
2642 ok = 0
2643 if rc14 == 0 { if out14[0] == 0 { if out14[1] == 1 { ok = 1 } } }
2644 pass = pass + pj_check(14, ok, "neg-level-blocked-stuck" as *u8)
2645 // T15 risk KATs (today 2026-07-17, WCONF 120/1): f0 OVERDUE; f1/f2/f3 TIGHT; f5 OK; f4 done skipped
2646 let out15: *i64 = sys_mmap(48) as *i64
2647 let rc15: i64 = pj_cmd_risk(prefix2, "fence" as *u8, "2026-07-17" as *u8, out15)
2648 ok = 0
2649 if rc15 == 0 { if out15[0] == 1 { if out15[1] == 0 { if out15[2] == 3 { if out15[3] == 1 { if out15[4] == 0 { ok = 1 } } } } } }
2650 pass = pass + pj_check(15, ok, "risk-slack-kats" as *u8)
2651 // T16 risk without WCONF (demo, today 2026-07-13): t2 OVERDUE, t3 UNRATED (honest no-duration-claim)
2652 let out16: *i64 = sys_mmap(48) as *i64
2653 let rc16: i64 = pj_cmd_risk(prefix, "demo" as *u8, "2026-07-13" as *u8, out16)
2654 ok = 0
2655 if rc16 == 0 { if out16[0] == 1 { if out16[1] == 0 { if out16[2] == 0 { if out16[3] == 0 { if out16[4] == 1 { ok = 1 } } } } } }
2656 pass = pass + pj_check(16, ok, "risk-no-wconf-unrated" as *u8)
2657 // T17 evm KAT: scope 60/530 -> 113 permil; cpi 11300*1000/40000 -> 282; not over budget
2658 let out17: *i64 = sys_mmap(32) as *i64
2659 let rc17: i64 = pj_cmd_evm(prefix2, "fence" as *u8, out17)
2660 ok = 0
2661 if rc17 == 0 { if out17[0] == 113 { if out17[1] == 282 { if out17[2] == 0 { ok = 1 } } } }
2662 pass = pass + pj_check(17, ok, "evm-kat" as *u8)
2663 // T18 NEG: evm without a BUDGET record fails loud
2664 let out18: *i64 = sys_mmap(32) as *i64
2665 let rc18: i64 = pj_cmd_evm(prefix, "demo" as *u8, out18)
2666 ok = 0
2667 if rc18 != 0 { ok = 1 }
2668 pass = pass + pj_check(18, ok, "neg-evm-no-budget" as *u8)
2669 // T19 html: contractor bo sees f2+f3 only; own note visible, amber's note HIDDEN; own timesheet row
2670 let rc19: i64 = pj_html(prefix2, "fence" as *u8, "bo" as *u8, "2026-07-17" as *u8, htmlfile)
2671 ok = 0
2672 if rc19 == 0 {
2673 let hszp: *i64 = sys_mmap(16) as *i64
2674 let hbuf: *u8 = ss_readall(htmlfile, hszp)
2675 let hsz: i64 = hszp[0]
2676 if hsz > 0 {
2677 let c1: i64 = pj_contains(hbuf, hsz, "<td>f2</td>" as *u8)
2678 let c2: i64 = pj_contains(hbuf, hsz, "<td>f3</td>" as *u8)
2679 let c3: i64 = pj_contains(hbuf, hsz, "<td>f1</td>" as *u8)
2680 let c4: i64 = pj_contains(hbuf, hsz, "<script" as *u8)
2681 let c5: i64 = pj_contains(hbuf, hsz, "<table" as *u8)
2682 let c6: i64 = pj_contains(hbuf, hsz, "posts delivered late" as *u8)
2683 let c7: i64 = pj_contains(hbuf, hsz, "rails cut and primed" as *u8)
2684 let c8: i64 = pj_contains(hbuf, hsz, "Timesheet" as *u8)
2685 if c1 == 1 { if c2 == 1 { if c3 == 0 { if c4 == 0 { if c5 == 1 { if c6 == 0 { if c7 == 1 { if c8 == 1 { ok = 1 } } } } } } } }
2686 }
2687 }
2688 pass = pass + pj_check(19, ok, "html-scoped-notes-timesheet" as *u8)
2689 // T20 timesheet KAT: bo 90 + amber 45 = 135 across 2 persons
2690 let out20: *i64 = sys_mmap(32) as *i64
2691 let rc20: i64 = pj_cmd_timesheet(prefix2, "fence" as *u8, out20)
2692 ok = 0
2693 if rc20 == 0 { if out20[0] == 135 { if out20[1] == 2 { ok = 1 } } }
2694 pass = pass + pj_check(20, ok, "timesheet-kat" as *u8)
2695 // T21 notes: f2 has exactly one; unknown tid fails loud
2696 let out21: *i64 = sys_mmap(32) as *i64
2697 let rc21: i64 = pj_cmd_notes(prefix2, "fence" as *u8, "f2" as *u8, out21)
2698 let out21b: *i64 = sys_mmap(32) as *i64
2699 let rc21b: i64 = pj_cmd_notes(prefix2, "fence" as *u8, "zz9" as *u8, out21b)
2700 ok = 0
2701 if rc21 == 0 { if out21[0] == 1 { if rc21b != 0 { ok = 1 } } }
2702 pass = pass + pj_check(21, ok, "notes-roundtrip-neg" as *u8)
2703 // T22 template: blueprint -> dated TASK/MILE file -> the REAL planner loads it (E2E)
2704 let tplf: *u8 = sys_mmap(256)
2705 o = 0
2706 o = pj_cat(tplf, o, "/tmp/pjt_" as *u8)
2707 o = pj_catn(tplf, o, uniq)
2708 o = pj_cat(tplf, o, "_tpl.txt" as *u8)
2709 tplf[o] = 0 as u8
2710 let genf: *u8 = sys_mmap(256)
2711 o = 0
2712 o = pj_cat(genf, o, "/tmp/pjt_" as *u8)
2713 o = pj_catn(genf, o, uniq)
2714 o = pj_cat(genf, o, "_gen.txt" as *u8)
2715 genf[o] = 0 as u8
2716 let prefixC: *u8 = sys_mmap(256)
2717 o = 0
2718 o = pj_cat(prefixC, o, "/tmp/pjt_" as *u8)
2719 o = pj_catn(prefixC, o, uniq)
2720 o = pj_cat(prefixC, o, "_stC-" as *u8)
2721 prefixC[o] = 0 as u8
2722 pj_wfile(tplf, "TPLTASK t1 amber 0 60 3 - dig\nTPLTASK t2 bo 9 120 4 t1 pour\nTPLMILE m1 12 done-pour\n" as *u8)
2723 let av22: *i64 = sys_mmap(8 * 8) as *i64
2724 av22[0] = "pj" as *u8 as i64
2725 av22[1] = "template" as *u8 as i64
2726 av22[2] = tplf as i64
2727 av22[3] = "shed" as *u8 as i64
2728 av22[4] = "2026-07-25" as *u8 as i64
2729 av22[5] = genf as i64
2730 let rc22: i64 = pj_cmd_template(6, av22)
2731 ok = 0
2732 if rc22 == 0 {
2733 let gszp: *i64 = sys_mmap(16) as *i64
2734 let gbuf: *u8 = ss_readall(genf, gszp)
2735 let gsz: i64 = gszp[0]
2736 if gsz > 0 {
2737 let k1: i64 = pj_contains(gbuf, gsz, "TASK shed t1 amber 2026-07-25 60 3 - dig" as *u8)
2738 let k2: i64 = pj_contains(gbuf, gsz, "TASK shed t2 bo 2026-08-03 120 4 t1 pour" as *u8)
2739 let k3: i64 = pj_contains(gbuf, gsz, "MILE shed m1 2026-08-06 done-pour" as *u8)
2740 if k1 == 1 { if k2 == 1 { if k3 == 1 {
2741 let a22: *i64 = sys_mmap(8 * 8) as *i64
2742 a22[0] = "load" as *u8 as i64
2743 a22[1] = genf as i64
2744 a22[2] = prefixC as i64
2745 a22[3] = "7401" as *u8 as i64
2746 a22[4] = 0
2747 let rc22b: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a22, outf)
2748 if rc22b == 0 {
2749 let st22: *i64 = pj_st_new()
2750 pj_gather(prefixC, "shed" as *u8, st22)
2751 if st22[0] == 2 { ok = 1 }
2752 }
2753 } } }
2754 }
2755 }
2756 pass = pass + pj_check(22, ok, "template-to-planner-e2e" as *u8)
2757 // T23 owner html carries the schedule bars + risk summary
2758 let htmlf2: *u8 = sys_mmap(256)
2759 o = 0
2760 o = pj_cat(htmlf2, o, "/tmp/pjt_" as *u8)
2761 o = pj_catn(htmlf2, o, uniq)
2762 o = pj_cat(htmlf2, o, "_page2.html" as *u8)
2763 htmlf2[o] = 0 as u8
2764 let rc23: i64 = pj_html(prefix2, "fence" as *u8, "elder" as *u8, "2026-07-17" as *u8, htmlf2)
2765 ok = 0
2766 if rc23 == 0 {
2767 let hszp2: *i64 = sys_mmap(16) as *i64
2768 let hbuf2: *u8 = ss_readall(htmlf2, hszp2)
2769 let hsz2: i64 = hszp2[0]
2770 if hsz2 > 0 {
2771 let d1: i64 = pj_contains(hbuf2, hsz2, "Schedule (latest-safe windows)" as *u8)
2772 let d2: i64 = pj_contains(hbuf2, hsz2, "risk: overdue=" as *u8)
2773 let d3: i64 = pj_contains(hbuf2, hsz2, "posts delivered late" as *u8)
2774 if d1 == 1 { if d2 == 1 { if d3 == 1 { ok = 1 } } }
2775 }
2776 }
2777 pass = pass + pj_check(23, ok, "html-gantt-risk-owner" as *u8)
2778 // T24 NEG: non-digit LOG minutes refused
2779 let badl: *u8 = sys_mmap(256)
2780 o = 0
2781 o = pj_cat(badl, o, "/tmp/pjt_" as *u8)
2782 o = pj_catn(badl, o, uniq)
2783 o = pj_cat(badl, o, "_badlog.txt" as *u8)
2784 badl[o] = 0 as u8
2785 pj_wfile(badl, "LOG fence f2 bo 2026-07-16 abc\n" as *u8)
2786 let av24: *i64 = sys_mmap(8 * 8) as *i64
2787 av24[0] = "pj" as *u8 as i64
2788 av24[1] = "load" as *u8 as i64
2789 av24[2] = badl as i64
2790 av24[3] = prefix2 as i64
2791 av24[4] = "7104" as *u8 as i64
2792 let rc24: i64 = pj_cmd_load(5, av24)
2793 ok = 0
2794 if rc24 != 0 { ok = 1 }
2795 pass = pass + pj_check(24, ok, "neg-bad-log-minutes" as *u8)
2796 // T25 ASSIGN override: t2 (bob's) reassigned to carol -> effective owner flips everywhere
2797 let asgf: *u8 = sys_mmap(256)
2798 o = 0
2799 o = pj_cat(asgf, o, "/tmp/pjt_" as *u8)
2800 o = pj_catn(asgf, o, uniq)
2801 o = pj_cat(asgf, o, "_asg.txt" as *u8)
2802 asgf[o] = 0 as u8
2803 pj_wfile(asgf, "ASSIGN demo t2 carol 2026-07-15\n" as *u8)
2804 let av25: *i64 = sys_mmap(8 * 8) as *i64
2805 av25[0] = "pj" as *u8 as i64
2806 av25[1] = "load" as *u8 as i64
2807 av25[2] = asgf as i64
2808 av25[3] = prefix as i64
2809 av25[4] = "7501" as *u8 as i64
2810 let rc25: i64 = pj_cmd_load(5, av25)
2811 let out25: *i64 = sys_mmap(32) as *i64
2812 let rc25b: i64 = pj_view(prefix, "demo" as *u8, "carol" as *u8, out25)
2813 let w25: i64 = pj_open_min(prefix, "demo" as *u8, "carol" as *u8)
2814 ok = 0
2815 if rc25 == 0 { if rc25b == 0 { if out25[1] == 2 { if w25 == 210 { ok = 1 } } } }
2816 pass = pass + pj_check(25, ok, "assign-effective-owner" as *u8)
2817 // T26 level APPLY: the fence move becomes ASSIGN records; a re-run finds nothing left to move
2818 let out26: *i64 = sys_mmap(32) as *i64
2819 let rc26: i64 = pj_cmd_level(prefix2, "fence" as *u8, out26, "2026-07-18" as *u8)
2820 let out26b: *i64 = sys_mmap(32) as *i64
2821 let rc26b: i64 = pj_cmd_level(prefix2, "fence" as *u8, out26b, "-" as *u8)
2822 let st26: *i64 = pj_st_new()
2823 pj_gather(prefix2, "fence" as *u8, st26)
2824 let t26i: i64 = pj_find(st26[1] as *i64, st26[0], "f2" as *u8)
2825 var f2owner_ok: i64 = 0
2826 if t26i >= 0 {
2827 let ow26: *i64 = st26[2] as *i64
2828 if pj_eq(ow26[t26i] as *u8, "cade" as *u8) == 1 { f2owner_ok = 1 }
2829 }
2830 ok = 0
2831 if rc26 == 0 { if out26[0] == 1 { if rc26b == 0 { if out26b[0] == 0 { if out26b[1] == 0 { if f2owner_ok == 1 { ok = 1 } } } } } }
2832 pass = pass + pj_check(26, ok, "level-apply-assign-e2e" as *u8)
2833 // T27 latest-date wins (an EARLIER assign never regresses the owner) + NEG bad assign date
2834 let asgf2: *u8 = sys_mmap(256)
2835 o = 0
2836 o = pj_cat(asgf2, o, "/tmp/pjt_" as *u8)
2837 o = pj_catn(asgf2, o, uniq)
2838 o = pj_cat(asgf2, o, "_asg2.txt" as *u8)
2839 asgf2[o] = 0 as u8
2840 pj_wfile(asgf2, "ASSIGN demo t2 dave 2026-07-14\n" as *u8)
2841 let av27: *i64 = sys_mmap(8 * 8) as *i64
2842 av27[0] = "pj" as *u8 as i64
2843 av27[1] = "load" as *u8 as i64
2844 av27[2] = asgf2 as i64
2845 av27[3] = prefix as i64
2846 av27[4] = "7502" as *u8 as i64
2847 let rc27: i64 = pj_cmd_load(5, av27)
2848 let st27: *i64 = pj_st_new()
2849 pj_gather(prefix, "demo" as *u8, st27)
2850 let t27i: i64 = pj_find(st27[1] as *i64, st27[0], "t2" as *u8)
2851 var still_carol: i64 = 0
2852 if t27i >= 0 {
2853 let ow27: *i64 = st27[2] as *i64
2854 if pj_eq(ow27[t27i] as *u8, "carol" as *u8) == 1 { still_carol = 1 }
2855 }
2856 let badaf: *u8 = sys_mmap(256)
2857 o = 0
2858 o = pj_cat(badaf, o, "/tmp/pjt_" as *u8)
2859 o = pj_catn(badaf, o, uniq)
2860 o = pj_cat(badaf, o, "_badasg.txt" as *u8)
2861 badaf[o] = 0 as u8
2862 pj_wfile(badaf, "ASSIGN demo t2 carol not-a-date\n" as *u8)
2863 let av27b: *i64 = sys_mmap(8 * 8) as *i64
2864 av27b[0] = "pj" as *u8 as i64
2865 av27b[1] = "load" as *u8 as i64
2866 av27b[2] = badaf as i64
2867 av27b[3] = prefix as i64
2868 av27b[4] = "7503" as *u8 as i64
2869 let rc27b: i64 = pj_cmd_load(5, av27b)
2870 ok = 0
2871 if rc27 == 0 { if still_carol == 1 { if rc27b != 0 { ok = 1 } } }
2872 pass = pass + pj_check(27, ok, "assign-latest-wins-neg" as *u8)
2873
2874 // T28 snapshot -> BASE records; T29 baseline variance detects a due slip
2875 let snapf: *u8 = sys_mmap(256)
2876 o = 0
2877 o = pj_cat(snapf, o, "/tmp/pjt_" as *u8)
2878 o = pj_catn(snapf, o, uniq)
2879 o = pj_cat(snapf, o, "_snap.txt" as *u8)
2880 snapf[o] = 0 as u8
2881 let rc28: i64 = pj_cmd_snapshot(prefix2, "fence" as *u8, "2026-07-05" as *u8, snapf)
2882 let av28: *i64 = sys_mmap(8 * 8) as *i64
2883 av28[0] = "pj" as *u8 as i64
2884 av28[1] = "load" as *u8 as i64
2885 av28[2] = snapf as i64
2886 av28[3] = prefix2 as i64
2887 av28[4] = "7601" as *u8 as i64
2888 let rc28b: i64 = pj_cmd_load(5, av28)
2889 ok = 0
2890 if rc28 == 0 { if rc28b == 0 {
2891 let st28: *i64 = pj_st_new()
2892 pj_gather(prefix2, "fence" as *u8, st28)
2893 if st28[36] == 6 { ok = 1 }
2894 } }
2895 pass = pass + pj_check(28, ok, "snapshot-writes-baseline" as *u8)
2896 // T29 slip detection in a FRESH store: one task due 2026-07-20, baseline due 2026-07-12 -> +8 days slipped
2897 let prefixV: *u8 = sys_mmap(256)
2898 o = 0
2899 o = pj_cat(prefixV, o, "/tmp/pjt_" as *u8)
2900 o = pj_catn(prefixV, o, uniq)
2901 o = pj_cat(prefixV, o, "_stV-" as *u8)
2902 prefixV[o] = 0 as u8
2903 let vplan: *u8 = sys_mmap(256)
2904 o = 0
2905 o = pj_cat(vplan, o, "/tmp/pjt_" as *u8)
2906 o = pj_catn(vplan, o, uniq)
2907 o = pj_cat(vplan, o, "_vplan.txt" as *u8)
2908 vplan[o] = 0 as u8
2909 let vproj: *u8 = sys_mmap(256)
2910 o = 0
2911 o = pj_cat(vproj, o, "/tmp/pjt_" as *u8)
2912 o = pj_catn(vproj, o, uniq)
2913 o = pj_cat(vproj, o, "_vproj.txt" as *u8)
2914 vproj[o] = 0 as u8
2915 pj_wfile(vplan, "TASK dam d1 amber 2026-07-20 100 3 - build\n" as *u8)
2916 pj_wfile(vproj, "PROJ dam elder dam-build\nMEMBER dam amber team -\nBASE dam d1 2026-07-12 100 2026-07-01\n" as *u8)
2917 let avp: *i64 = sys_mmap(8 * 8) as *i64
2918 avp[0] = "load" as *u8 as i64
2919 avp[1] = vplan as i64
2920 avp[2] = prefixV as i64
2921 avp[3] = "7602" as *u8 as i64
2922 avp[4] = 0
2923 let rc29p: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, avp, outf)
2924 let av29: *i64 = sys_mmap(8 * 8) as *i64
2925 av29[0] = "pj" as *u8 as i64
2926 av29[1] = "load" as *u8 as i64
2927 av29[2] = vproj as i64
2928 av29[3] = prefixV as i64
2929 av29[4] = "7603" as *u8 as i64
2930 let rc29: i64 = pj_cmd_load(5, av29)
2931 let out29: *i64 = sys_mmap(32) as *i64
2932 let rc29b: i64 = pj_cmd_baseline(prefixV, "dam" as *u8, out29)
2933 ok = 0
2934 if rc29 == 0 { if rc29p == 0 { if rc29b == 0 { if out29[0] == 1 { if out29[1] == 8 { if out29[2] == 1 { ok = 1 } } } } } }
2935 pass = pass + pj_check(29, ok, "baseline-slip-detected" as *u8)
2936 // T30 burndown: fence has f4 DONE (effort 60); remaining = total - 60, done_points=1
2937 let out30: *i64 = sys_mmap(32) as *i64
2938 let rc30: i64 = pj_cmd_burndown(prefix2, "fence" as *u8, out30)
2939 ok = 0
2940 if rc30 == 0 { if out30[1] == 60 { if out30[3] == 1 { if out30[2] == out30[0] - 60 { ok = 1 } } } }
2941 pass = pass + pj_check(30, ok, "burndown-from-done" as *u8)
2942 // T31 NEG: baseline with no BASE records fails loud (demo has none)
2943 let out31: *i64 = sys_mmap(32) as *i64
2944 let rc31: i64 = pj_cmd_baseline(prefix, "demo" as *u8, out31)
2945 ok = 0
2946 if rc31 != 0 { ok = 1 }
2947 pass = pass + pj_check(31, ok, "neg-baseline-none" as *u8)
2948
2949 let rep: *u8 = sys_mmap(256)
2950 var z: i64 = 0
2951 z = pj_cat(rep, z, "NX-PROJECT-GATE pass=" as *u8)
2952 z = pj_catn(rep, z, pass)
2953 z = pj_cat(rep, z, "/31 verdict=" as *u8)
2954 if pass == 31 { z = pj_cat(rep, z, "GREEN\n" as *u8) } else { z = pj_cat(rep, z, "RED\n" as *u8) }
2955 sys_write(1, rep, z)
2956 if pass == 31 { return 0 }
2957 return 1
2958}
2959
2960// ---------------- CLI ----------------
2961func pj_usage() -> i64 {
2962 pj_p("nx_project: load | view | workload | level [apply <date>] | risk <today> | evm | timesheet | notes <tid> | template <tpl> <pid> <start> <out> | snapshot <prefix> <pid> <date> <out> | baseline <prefix> <pid> | burndown <prefix> <pid> | html <prefix> <pid> <person> <today> <out|-> | (argless = selftest)\n" as *u8)
2963 return 1
2964}
2965
2966func pj_cli(argc: i64, argv: *i64) -> i64 {
2967 if argc < 2 { return pj_selftest() }
2968 let cmd: *u8 = argv[1] as *u8
2969 if pj_eq(cmd, "load" as *u8) == 1 { return pj_cmd_load(argc, argv) }
2970 if pj_eq(cmd, "view" as *u8) == 1 {
2971 if argc < 5 { pj_p("PROJECT view needs <prefix> <pid> <person> -- fail loud\n" as *u8); return 1 }
2972 let out: *i64 = sys_mmap(32) as *i64
2973 return pj_view(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, out)
2974 }
2975 if pj_eq(cmd, "workload" as *u8) == 1 {
2976 if argc < 4 { pj_p("PROJECT workload needs <prefix> <pid> -- fail loud\n" as *u8); return 1 }
2977 return pj_cmd_workload(argv[2] as *u8, argv[3] as *u8)
2978 }
2979 if pj_eq(cmd, "level" as *u8) == 1 {
2980 if argc < 4 { pj_p("PROJECT level needs <prefix> <pid> [apply <date>] -- fail loud\n" as *u8); return 1 }
2981 let out2: *i64 = sys_mmap(32) as *i64
2982 var aply: *u8 = "-" as *u8
2983 if argc >= 6 { if pj_eq(argv[4] as *u8, "apply" as *u8) == 1 { aply = argv[5] as *u8 } }
2984 return pj_cmd_level(argv[2] as *u8, argv[3] as *u8, out2, aply)
2985 }
2986 if pj_eq(cmd, "risk" as *u8) == 1 {
2987 if argc < 5 { pj_p("PROJECT risk needs <prefix> <pid> <today> -- fail loud\n" as *u8); return 1 }
2988 let out3: *i64 = sys_mmap(48) as *i64
2989 return pj_cmd_risk(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, out3)
2990 }
2991 if pj_eq(cmd, "evm" as *u8) == 1 {
2992 if argc < 4 { pj_p("PROJECT evm needs <prefix> <pid> -- fail loud\n" as *u8); return 1 }
2993 let out4: *i64 = sys_mmap(32) as *i64
2994 return pj_cmd_evm(argv[2] as *u8, argv[3] as *u8, out4)
2995 }
2996 if pj_eq(cmd, "html" as *u8) == 1 {
2997 if argc < 7 { pj_p("PROJECT html needs <prefix> <pid> <person> <today> <outfile|-> -- fail loud\n" as *u8); return 1 }
2998 return pj_html(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8)
2999 }
3000 if pj_eq(cmd, "timesheet" as *u8) == 1 {
3001 if argc < 4 { pj_p("PROJECT timesheet needs <prefix> <pid> -- fail loud\n" as *u8); return 1 }
3002 let out5: *i64 = sys_mmap(32) as *i64
3003 return pj_cmd_timesheet(argv[2] as *u8, argv[3] as *u8, out5)
3004 }
3005 if pj_eq(cmd, "notes" as *u8) == 1 {
3006 if argc < 5 { pj_p("PROJECT notes needs <prefix> <pid> <tid> -- fail loud\n" as *u8); return 1 }
3007 let out6: *i64 = sys_mmap(32) as *i64
3008 return pj_cmd_notes(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, out6)
3009 }
3010 if pj_eq(cmd, "template" as *u8) == 1 { return pj_cmd_template(argc, argv) }
3011 if pj_eq(cmd, "snapshot" as *u8) == 1 {
3012 if argc < 6 { pj_p("PROJECT snapshot needs <prefix> <pid> <date> <outfile> -- fail loud\n" as *u8); return 1 }
3013 return pj_cmd_snapshot(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8)
3014 }
3015 if pj_eq(cmd, "baseline" as *u8) == 1 {
3016 if argc < 4 { pj_p("PROJECT baseline needs <prefix> <pid> -- fail loud\n" as *u8); return 1 }
3017 let out7: *i64 = sys_mmap(32) as *i64
3018 return pj_cmd_baseline(argv[2] as *u8, argv[3] as *u8, out7)
3019 }
3020 if pj_eq(cmd, "burndown" as *u8) == 1 {
3021 if argc < 4 { pj_p("PROJECT burndown needs <prefix> <pid> -- fail loud\n" as *u8); return 1 }
3022 let out8: *i64 = sys_mmap(32) as *i64
3023 return pj_cmd_burndown(argv[2] as *u8, argv[3] as *u8, out8)
3024 }
3025 return pj_usage()
3026}