nx_pipeline_banner.nx source
↩ module page · 242 lines · 11331 B
1// nx_pipeline_banner.nx -- HUB primitive; renders per-article pipeline-
2// stage banner emitting "what stage is this artifact at + what's missing
3// for next transition".
4//
5// Per V2.0 PIPELINE BOOTSTRAP PLAN P-4: every wiki article shows its
6// pipeline stage at the top. Operator visiting /wiki/<doc-name> sees
7// immediate signal: how mature is this content?
8//
9// COMPOSES (HUB primitives only):
10// wiki/nx_artifact_store (NxArtifactStore + per-artifact taxonomy + stage)
11// nx_html_escape (safe value emission)
12//
13// COMPOSED BY:
14// wiki/nx_wiki_doc_handler (every /wiki/<doc-name> page)
15// (future) obd/nx_obd_doc_handler
16// (future) sprinkler/nx_sprinkler_doc_handler
17// (future) third-party hosted site doc handlers
18//
19// V2.0 SCOPE:
20// - Lookup doc by URL in NxArtifactStore
21// - Sealed 5-color stage badge
22// - Per-stage criteria-met + criteria-missing list (operator self-audit)
23// - "(not in artifact store)" hint when lookup misses (informational)
24//
25// V3+ SCOPE (TODO):
26// - "Advance this artifact" admin button (operator one-click stage promote)
27// - Per-criterion hint links ("add Quadrant front-matter -> see Diátaxis")
28// - History timeline (when did stage last change?)
29//
30// Status: V2.0. 2026-05-27.
31
32import "nx_syscalls.nx"
33import "nx_html_escape.nx"
34import "wiki/nx_artifact_store.nx"
35
36// ===== Sealed verdict surface (codes 2980-2989) =================================================
37const NX_PBAN_OK: i64 = 0
38const NX_PBAN_BAD_INPUT: i64 = 2980
39const NX_PBAN_OUTPUT_OVERFLOW: i64 = 2981
40
41// ===== Named constants (M7) =================================================
42const NX_PBAN_MAX_DOC_PATH: i64 = 512
43
44// Sealed stage color CSS classes (per V1 page format §3.4 chrome).
45const NX_PBAN_CLASS_UNSTRUCTURED: *u8 = "stage-banner stage-unstructured" as *u8
46const NX_PBAN_CLASS_STRUCTURED: *u8 = "stage-banner stage-structured" as *u8
47const NX_PBAN_CLASS_MEANINGFUL: *u8 = "stage-banner stage-meaningful" as *u8
48const NX_PBAN_CLASS_ACTIONABLE: *u8 = "stage-banner stage-actionable" as *u8
49const NX_PBAN_CLASS_REPEATABLE: *u8 = "stage-banner stage-repeatable" as *u8
50const NX_PBAN_CLASS_UNKNOWN: *u8 = "stage-banner stage-unknown" as *u8
51
52// ===== Lookup stage color class for a stage value =================================================
53
54func nx_pban_class_for_stage(stage: i64) -> *u8 {
55 if stage == NX_ART_STAGE_UNSTRUCTURED { return NX_PBAN_CLASS_UNSTRUCTURED }
56 if stage == NX_ART_STAGE_STRUCTURED { return NX_PBAN_CLASS_STRUCTURED }
57 if stage == NX_ART_STAGE_MEANINGFUL { return NX_PBAN_CLASS_MEANINGFUL }
58 if stage == NX_ART_STAGE_ACTIONABLE { return NX_PBAN_CLASS_ACTIONABLE }
59 if stage == NX_ART_STAGE_REPEATABLE { return NX_PBAN_CLASS_REPEATABLE }
60 return NX_PBAN_CLASS_UNKNOWN
61}
62
63// ===== Output writer (bounded; M5) =================================================
64
65func nx_pban_put_raw(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 {
66 if off < 0 { return 0 - NX_PBAN_BAD_INPUT }
67 if off + n > cap { return 0 - NX_PBAN_OUTPUT_OVERFLOW }
68 var i: i64 = 0
69 while i < n { out[off + i] = src[i]; i = i + 1 }
70 return off + n
71}
72
73func nx_pban_put_z(out: *u8, cap: i64, off: i64, s: *u8) -> i64 {
74 var n: i64 = 0
75 while s[n] != (0 as u8) {
76 if n >= cap { return 0 - NX_PBAN_OUTPUT_OVERFLOW }
77 n = n + 1
78 }
79 return nx_pban_put_raw(out, cap, off, s, n)
80}
81
82func nx_pban_put_escaped(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 {
83 if n < 1 { return off }
84 if cap - off < n * 6 + 1 { return 0 - NX_PBAN_OUTPUT_OVERFLOW }
85 let w: i64 = html_escape((out as i64 + off) as *u8, cap - off, src, n)
86 if w < 0 { return 0 - NX_PBAN_OUTPUT_OVERFLOW }
87 return off + w
88}
89
90// ===== Per-stage criteria list (what's met / what's missing) =================================================
91//
92// Per V2_0_PIPELINE_BOOTSTRAP_ARC_PLAN §2 transitions:
93// STRUCTURED needs: Quadrant + Topic Type + Status all present
94// MEANINGFUL needs: + Composes count > 0
95// ACTIONABLE needs: + Trust State > WRITTEN_UNTESTED + Competitive State != NONE
96// REPEATABLE needs: + Trust = PERSONALLY_MEASURED + Comp ∈ {MATCHES, BEATS}
97
98func nx_pban_render_criteria(out: *u8, cap: i64, off: i64,
99 store: *NxArtifactStore, rowid: i64) -> i64 {
100 var o: i64 = off
101
102 let q: i64 = store.quadrant[rowid]
103 let tt: i64 = store.topic_type[rowid]
104 let st: i64 = store.status[rowid]
105 let tr: i64 = store.trust_state[rowid]
106 let co: i64 = store.competitive_state[rowid]
107 let nc: i64 = store.composes_count[rowid]
108
109 o = nx_pban_put_z(out, cap, o, "<ul class=\"pipeline-criteria\">" as *u8); if o < 0 { return o }
110
111 // Quadrant
112 o = nx_pban_put_z(out, cap, o, "<li>" as *u8); if o < 0 { return o }
113 if q != NX_ART_QUAD_NONE { o = nx_pban_put_z(out, cap, o, "<b>OK</b> " as *u8) }
114 if q == NX_ART_QUAD_NONE { o = nx_pban_put_z(out, cap, o, "<b>MISSING</b> " as *u8) }
115 if o < 0 { return o }
116 o = nx_pban_put_z(out, cap, o, "Quadrant (Diátaxis)" as *u8); if o < 0 { return o }
117 o = nx_pban_put_z(out, cap, o, "</li>" as *u8); if o < 0 { return o }
118
119 // Topic Type
120 o = nx_pban_put_z(out, cap, o, "<li>" as *u8); if o < 0 { return o }
121 if tt != NX_ART_DITA_NONE { o = nx_pban_put_z(out, cap, o, "<b>OK</b> " as *u8) }
122 if tt == NX_ART_DITA_NONE { o = nx_pban_put_z(out, cap, o, "<b>MISSING</b> " as *u8) }
123 if o < 0 { return o }
124 o = nx_pban_put_z(out, cap, o, "Topic Type (DITA)" as *u8); if o < 0 { return o }
125 o = nx_pban_put_z(out, cap, o, "</li>" as *u8); if o < 0 { return o }
126
127 // Status
128 o = nx_pban_put_z(out, cap, o, "<li>" as *u8); if o < 0 { return o }
129 if st != NX_ART_STATUS_NONE { o = nx_pban_put_z(out, cap, o, "<b>OK</b> " as *u8) }
130 if st == NX_ART_STATUS_NONE { o = nx_pban_put_z(out, cap, o, "<b>MISSING</b> " as *u8) }
131 if o < 0 { return o }
132 o = nx_pban_put_z(out, cap, o, "Status" as *u8); if o < 0 { return o }
133 o = nx_pban_put_z(out, cap, o, "</li>" as *u8); if o < 0 { return o }
134
135 // Composes (for MEANINGFUL)
136 o = nx_pban_put_z(out, cap, o, "<li>" as *u8); if o < 0 { return o }
137 if nc > 0 { o = nx_pban_put_z(out, cap, o, "<b>OK</b> " as *u8) }
138 if nc == 0 { o = nx_pban_put_z(out, cap, o, "<b>MISSING</b> " as *u8) }
139 if o < 0 { return o }
140 o = nx_pban_put_z(out, cap, o, "Composes ([[X]] refs) for MEANINGFUL" as *u8); if o < 0 { return o }
141 o = nx_pban_put_z(out, cap, o, "</li>" as *u8); if o < 0 { return o }
142
143 // Trust State (for ACTIONABLE)
144 o = nx_pban_put_z(out, cap, o, "<li>" as *u8); if o < 0 { return o }
145 if tr > NX_ART_TRUST_WRITTEN_UNTESTED { o = nx_pban_put_z(out, cap, o, "<b>OK</b> " as *u8) }
146 if tr <= NX_ART_TRUST_WRITTEN_UNTESTED { o = nx_pban_put_z(out, cap, o, "<b>MISSING</b> " as *u8) }
147 if o < 0 { return o }
148 o = nx_pban_put_z(out, cap, o, "Trust State > WRITTEN_UNTESTED for ACTIONABLE" as *u8); if o < 0 { return o }
149 o = nx_pban_put_z(out, cap, o, "</li>" as *u8); if o < 0 { return o }
150
151 // Competitive State (for ACTIONABLE)
152 o = nx_pban_put_z(out, cap, o, "<li>" as *u8); if o < 0 { return o }
153 if co != NX_ART_COMP_NONE { o = nx_pban_put_z(out, cap, o, "<b>OK</b> " as *u8) }
154 if co == NX_ART_COMP_NONE { o = nx_pban_put_z(out, cap, o, "<b>MISSING</b> " as *u8) }
155 if o < 0 { return o }
156 o = nx_pban_put_z(out, cap, o, "Competitive State for ACTIONABLE" as *u8); if o < 0 { return o }
157 o = nx_pban_put_z(out, cap, o, "</li>" as *u8); if o < 0 { return o }
158
159 // PERSONALLY_MEASURED + MATCHES|BEATS (for REPEATABLE)
160 o = nx_pban_put_z(out, cap, o, "<li>" as *u8); if o < 0 { return o }
161 var repro_ok: i64 = 0
162 if tr == NX_ART_TRUST_PERSONALLY_MEASURED {
163 if co >= NX_ART_COMP_MATCHES { repro_ok = 1 }
164 }
165 if repro_ok == 1 { o = nx_pban_put_z(out, cap, o, "<b>OK</b> " as *u8) }
166 if repro_ok == 0 { o = nx_pban_put_z(out, cap, o, "<b>MISSING</b> " as *u8) }
167 if o < 0 { return o }
168 o = nx_pban_put_z(out, cap, o, "PERSONALLY_MEASURED + Competitive State >= MATCHES for REPEATABLE" as *u8); if o < 0 { return o }
169 o = nx_pban_put_z(out, cap, o, "</li>" as *u8); if o < 0 { return o }
170
171 o = nx_pban_put_z(out, cap, o, "</ul>\n" as *u8); if o < 0 { return o }
172 return o
173}
174
175// ===== Top-level: render banner for a doc =================================================
176//
177// Caller supplies the canonical doc PATH (filesystem path, matching
178// what was passed to nx_pipeline_walker). If lookup fails, renders a
179// brief informational note instead of an error.
180//
181// The doc URL (e.g., "/wiki/nishi-search-charter") MAY differ from the
182// artifact path (e.g., "/mnt/c/Users/elder/nishi-silicon/NISHI_SEARCH_CHARTER.md").
183// V2.0 banner takes the artifact PATH so caller controls the mapping;
184// V3+ adds URL-keyed lookup.
185
186func nx_pipeline_banner_render(out: *u8, cap: i64, off: i64,
187 store: *NxArtifactStore,
188 doc_path: *u8, doc_path_n: i64,
189 out_off: *i64) -> i64 {
190 if (out as i64) == 0 { return 0 - NX_PBAN_BAD_INPUT }
191 if (out_off as i64) == 0 { return 0 - NX_PBAN_BAD_INPUT }
192 if doc_path_n < 0 { return 0 - NX_PBAN_BAD_INPUT }
193 if doc_path_n > NX_PBAN_MAX_DOC_PATH { return 0 - NX_PBAN_BAD_INPUT }
194
195 var o: i64 = off
196
197 // Lookup
198 if (store as i64) == 0 {
199 out_off[0] = o
200 return NX_PBAN_OK // no store; render nothing (silent skip)
201 }
202 if store.valid != 1 {
203 out_off[0] = o
204 return NX_PBAN_OK
205 }
206
207 let rowid: i64 = nx_artifact_store_find_by_path(store, doc_path, doc_path_n)
208 if rowid < 0 {
209 // Not in store -- brief informational note
210 o = nx_pban_put_z(out, cap, o,
211 "<div class=\"stage-banner stage-unknown\"><small>This article is not yet in the substrate pipeline store. <a href=\"/wiki/pipeline\">See pipeline overview</a>.</small></div>\n" as *u8)
212 if o < 0 { return o }
213 out_off[0] = o
214 return NX_PBAN_OK
215 }
216
217 let stage: i64 = nx_artifact_store_stage_at(store, rowid)
218 if stage < 0 {
219 out_off[0] = o
220 return NX_PBAN_OK
221 }
222
223 // Banner container
224 o = nx_pban_put_z(out, cap, o, "<div class=\"" as *u8); if o < 0 { return o }
225 o = nx_pban_put_z(out, cap, o, nx_pban_class_for_stage(stage)); if o < 0 { return o }
226 o = nx_pban_put_z(out, cap, o, "\">\n" as *u8); if o < 0 { return o }
227
228 // Stage label
229 o = nx_pban_put_z(out, cap, o, "<p><b>Pipeline stage:</b> <code>" as *u8); if o < 0 { return o }
230 o = nx_pban_put_z(out, cap, o, nx_art_stage_name(stage)); if o < 0 { return o }
231 o = nx_pban_put_z(out, cap, o, "</code> <small><a href=\"/wiki/pipeline\">see pipeline overview</a></small></p>\n" as *u8); if o < 0 { return o }
232
233 // Criteria checklist
234 o = nx_pban_put_z(out, cap, o,
235 "<p><small><b>5-stage pipeline criteria</b> (per <a href=\"/wiki/v2-0-pipeline-bootstrap-arc-plan\">V2.0 PLAN</a>):</small></p>\n" as *u8); if o < 0 { return o }
236 o = nx_pban_render_criteria(out, cap, o, store, rowid); if o < 0 { return o }
237
238 o = nx_pban_put_z(out, cap, o, "</div>\n" as *u8); if o < 0 { return o }
239
240 out_off[0] = o
241 return NX_PBAN_OK
242}