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}