code wiki / (root) / nx_doc_annotate.nx

nx_doc_annotate.nx source

↩ module page · 219 lines · 8882 B

1// nx_doc_annotate.nx -- LEGAL RUNG D4: the sovereign annotation / markup overlay. 2// 3// module: nishi-core.legal.doc_annotate 4// capability: LEGAL_DOC_ANNOTATE 5// 6// The "sticky-notes / markup / feedback / lists" half of the DocuSign-better 7// document experience. A FORMAT-AGNOSTIC overlay: it never parses the document, 8// it anchors annotations to a {doc_id, version, page} so it sits ON TOP of any 9// format (PDF / DOCX / our own doc format) without depending on it. Each 10// annotation is a note, threaded comment, redline (strike + propose), highlight, 11// checklist item, or signature-field placement, carried through an 12// open -> resolved/accepted/rejected/done review lifecycle with author 13// attribution and a resolver audit. 14// 15// The s-class safety invariant (the analog of D0/D5's never-void): once a 16// document's envelope is COMPLETED (signed), its overlay is IMMUTABLE -- you 17// cannot add or re-status an annotation on a signed agreement. The caller passes 18// `frozen` = (the doc's envelope is completed); add / resolve REFUSE when frozen. 19// Plus: review history is additive (Rule 13 -- rejected redlines are retained, 20// never deleted), annotations are pinned to the version they were made on (a 21// version bump does not silently move them), and a REQUIRED checklist gates the 22// envelope send (compose with D2: a portal checks nx_ann_checklist_complete 23// before nx_env_send). 24// 25// Representation: caller-allocated flat i64 record arrays (mirrors nx_doc_vault / 26// nx_doc_envelope; no internal allocation). Pure logic core -> self-contained gate. 27// 28// Composes (at the gate): nx_doc_envelope (D2 frozen=COMPLETED + checklist-gates- 29// send), nx_doc_vault (D1 versions). Distinct from nx_doc_envelope because: that 30// is the signing lifecycle; this is the in-document review/markup overlay around it. 31// license_tier: ORIGINAL 32// lineage_id: nishi_doc_annotate_d4 33import "nx_syscalls.nx" 34 35// ---- annotation types ---- 36const AN_NOTE: i64 = 0 // sticky note 37const AN_COMMENT: i64 = 1 // threaded comment (can have replies via NF_PARENT) 38const AN_REDLINE: i64 = 2 // strike + propose replacement (accept/reject) 39const AN_HIGHLIGHT: i64 = 3 40const AN_CHECK: i64 = 4 // checklist item (done via AS_DONE) 41const AN_SIGFIELD: i64 = 5 // signature-field placement (which signer, which page) 42 43// ---- annotation status (review lifecycle) ---- 44const AS_OPEN: i64 = 0 45const AS_RESOLVED: i64 = 1 // comment resolved 46const AS_ACCEPTED: i64 = 2 // redline adopted 47const AS_REJECTED: i64 = 3 // redline declined (retained -- additive) 48const AS_DONE: i64 = 4 // checklist item done / sigfield filled 49 50// ---- flags (bitmask) ---- 51const ANF_REQUIRED: i64 = 1 // a required checklist item (gates send) 52 53// ---- op results ---- 54const ANR_OK: i64 = 0 55const ANR_FROZEN: i64 = 1 // doc's envelope is COMPLETED -> overlay immutable 56const ANR_NOT_FOUND: i64 = 2 57const ANR_FULL: i64 = 3 58 59// ---- annotation record: flat_an[a*NF_STRIDE + NF_*] ---- 60const NF_ANN: i64 = 0 // annotation id 61const NF_DOC: i64 = 1 // doc id 62const NF_VER: i64 = 2 // document version this annotation is pinned to 63const NF_TYPE: i64 = 3 64const NF_AUTHOR: i64 = 4 65const NF_PAGE: i64 = 5 // page anchor (format-agnostic; pixel x/y = display layer) 66const NF_STATUS: i64 = 6 67const NF_PARENT: i64 = 7 // parent annotation id for threaded replies (0 = top-level) 68const NF_FLAGS: i64 = 8 69const NF_TS: i64 = 9 70const NF_RESOLVER: i64 = 10 // who accepted/rejected/resolved (audit) 71const NF_STRIDE: i64 = 11 72 73// ---- locate an annotation record index for ann_id, or -1. ---- 74func na_find(flat_an: *i64, na: i64, ann_id: i64) -> i64 { 75 var i: i64 = 0 76 while i < na { 77 if flat_an[i * NF_STRIDE + NF_ANN] == ann_id { return i } 78 i = i + 1 79 } 80 return 0 - 1 81} 82 83// ---- internal: write a fresh record at slot `na`. ---- 84func na_put(flat_an: *i64, na: i64, ann_id: i64, doc_id: i64, ver: i64, 85 atype: i64, author: i64, page: i64, parent: i64, flags: i64, ts: i64) -> i64 { 86 let b: i64 = na * NF_STRIDE 87 flat_an[b + NF_ANN] = ann_id 88 flat_an[b + NF_DOC] = doc_id 89 flat_an[b + NF_VER] = ver 90 flat_an[b + NF_TYPE] = atype 91 flat_an[b + NF_AUTHOR] = author 92 flat_an[b + NF_PAGE] = page 93 flat_an[b + NF_STATUS] = AS_OPEN 94 flat_an[b + NF_PARENT] = parent 95 flat_an[b + NF_FLAGS] = flags 96 flat_an[b + NF_TS] = ts 97 flat_an[b + NF_RESOLVER] = 0 98 return na + 1 99} 100 101// ---- add an annotation (note/comment/redline/highlight/check/sigfield). ---- 102// frozen=1 (the doc's envelope is COMPLETED) -> REFUSED, overlay is immutable. 103// Returns the new count, or -ANR_FROZEN / -ANR_FULL. 104func nx_ann_add(flat_an: *i64, na: i64, cap: i64, frozen: i64, 105 ann_id: i64, doc_id: i64, ver: i64, atype: i64, author: i64, 106 page: i64, flags: i64, ts: i64) -> i64 { 107 if frozen == 1 { return 0 - ANR_FROZEN } 108 if na >= cap { return 0 - ANR_FULL } 109 return na_put(flat_an, na, ann_id, doc_id, ver, atype, author, page, 0, flags, ts) 110} 111 112// ---- add a threaded reply to a parent comment. ---- 113func nx_ann_add_reply(flat_an: *i64, na: i64, cap: i64, frozen: i64, 114 ann_id: i64, doc_id: i64, ver: i64, author: i64, parent: i64, ts: i64) -> i64 { 115 if frozen == 1 { return 0 - ANR_FROZEN } 116 if na >= cap { return 0 - ANR_FULL } 117 return na_put(flat_an, na, ann_id, doc_id, ver, AN_COMMENT, author, 0, parent, 0, ts) 118} 119 120// ---- re-status an annotation (resolve/accept/reject/done). frozen=1 -> REFUSED. ---- 121// Additive: it changes status + records the resolver; it never removes a record. 122func nx_ann_resolve(flat_an: *i64, na: i64, frozen: i64, ann_id: i64, 123 new_status: i64, resolver: i64, ts: i64) -> i64 { 124 if frozen == 1 { return ANR_FROZEN } 125 let idx: i64 = na_find(flat_an, na, ann_id) 126 if idx < 0 { return ANR_NOT_FOUND } 127 let b: i64 = idx * NF_STRIDE 128 flat_an[b + NF_STATUS] = new_status 129 flat_an[b + NF_RESOLVER] = resolver 130 flat_an[b + NF_TS] = ts 131 return ANR_OK 132} 133 134func nx_ann_status(flat_an: *i64, na: i64, ann_id: i64) -> i64 { 135 let idx: i64 = na_find(flat_an, na, ann_id) 136 if idx < 0 { return 0 - 1 } 137 return flat_an[idx * NF_STRIDE + NF_STATUS] 138} 139 140func nx_ann_author(flat_an: *i64, na: i64, ann_id: i64) -> i64 { 141 let idx: i64 = na_find(flat_an, na, ann_id) 142 if idx < 0 { return 0 - 1 } 143 return flat_an[idx * NF_STRIDE + NF_AUTHOR] 144} 145 146// ---- count annotations on a doc + version (per-version pinning). ---- 147func nx_ann_count(flat_an: *i64, na: i64, doc_id: i64, ver: i64) -> i64 { 148 var n: i64 = 0 149 var i: i64 = 0 150 while i < na { 151 let b: i64 = i * NF_STRIDE 152 if flat_an[b + NF_DOC] == doc_id { if flat_an[b + NF_VER] == ver { n = n + 1 } } 153 i = i + 1 154 } 155 return n 156} 157 158// ---- count annotations of a given type on a doc + version. ---- 159func nx_ann_count_type(flat_an: *i64, na: i64, doc_id: i64, ver: i64, atype: i64) -> i64 { 160 var n: i64 = 0 161 var i: i64 = 0 162 while i < na { 163 let b: i64 = i * NF_STRIDE 164 if flat_an[b + NF_DOC] == doc_id { 165 if flat_an[b + NF_VER] == ver { 166 if flat_an[b + NF_TYPE] == atype { n = n + 1 } 167 } 168 } 169 i = i + 1 170 } 171 return n 172} 173 174// ---- required checklist items NOT yet done (0 => all required done). ---- 175func nx_ann_checklist_remaining(flat_an: *i64, na: i64, doc_id: i64, ver: i64) -> i64 { 176 var n: i64 = 0 177 var i: i64 = 0 178 while i < na { 179 let b: i64 = i * NF_STRIDE 180 if flat_an[b + NF_DOC] == doc_id { 181 if flat_an[b + NF_VER] == ver { 182 if flat_an[b + NF_TYPE] == AN_CHECK { 183 if (flat_an[b + NF_FLAGS] & ANF_REQUIRED) == ANF_REQUIRED { 184 if flat_an[b + NF_STATUS] != AS_DONE { n = n + 1 } 185 } 186 } 187 } 188 } 189 i = i + 1 190 } 191 return n 192} 193 194// ---- predicate: every required checklist item is done (gates D2 send). ---- 195func nx_ann_checklist_complete(flat_an: *i64, na: i64, doc_id: i64, ver: i64) -> i64 { 196 if nx_ann_checklist_remaining(flat_an, na, doc_id, ver) == 0 { return 1 } 197 return 0 198} 199 200// ---- replies to a parent comment (threaded discussion depth). ---- 201func nx_ann_thread_replies(flat_an: *i64, na: i64, parent_id: i64) -> i64 { 202 var n: i64 = 0 203 var i: i64 = 0 204 while i < na { 205 if flat_an[i * NF_STRIDE + NF_PARENT] == parent_id { n = n + 1 } 206 i = i + 1 207 } 208 return n 209} 210 211func nx_ann_type_name(t: i64) -> *u8 { 212 if t == AN_NOTE { return "NOTE" } 213 if t == AN_COMMENT { return "COMMENT" } 214 if t == AN_REDLINE { return "REDLINE" } 215 if t == AN_HIGHLIGHT { return "HIGHLIGHT" } 216 if t == AN_CHECK { return "CHECK" } 217 if t == AN_SIGFIELD { return "SIGFIELD" } 218 return "?" 219}