qualia_core_db/render/authoring.rs
1//! Phase 5 — the authoring vocabulary (`ns/ui`) + render **planner** *(the qapps upgrade; §6/§7)*.
2//!
3//! Upgrades the qapps model ([`docs/manuals/qapps_specification.md`]) from 2D-pane CSS-grid layouts
4//! to **manifold worlds**: a qapp declares *views over one manifold* (a 3D scene **and** a 2D pane
5//! from the SAME source), each carrying **governance** + **budget** annotations the engine
6//! **enforces at plan time** — before anything is drawn.
7//!
8//! This module is the **in-memory vocabulary + planner**. The wire-form (`yaml-ld-q42` →
9//! RDF → CBOR-LD → NQuin `@context` expansion) is task #8; **ShEx *describes*** the contract and
10//! **SHACL *enforces*** the shape (ADR 0009) — one source. Those are deliberately *not* duplicated
11//! here; this is the runtime the parsed document drives.
12//!
13//! ## The rails (RENDERER_DEFINITION §8) — enforced, not reinvented
14//! * **Governance primitives are the §8 substrate *surfaced*.** Rights-bounded refusal reuses the
15//! inherited `logic::deontic` gate (the same one Phases 3–4 use); this module authors **no** new
16//! normative rules — it only *applies* caller-supplied norms.
17//! * **Wisdom-out-of-band (attestation gates).** A view marked `requires_attestation` is
18//! **withheld** until an attestation is present — the human ratifies by attesting (a DID-signature
19//! trigger). Signature *verification* is the identity/key-vault layer's job (it fails closed
20//! there); this gate enforces the *presence* of an attestation, the out-of-band hook.
21//! * **Affordability at authoring time.** Budget is declared, not hoped-for: on a constrained
22//! device tier a `Scene3D` view **degrades to 2D** ([`ViewDisposition::Collapsed2D`]) rather than
23//! failing — graceful 3D→2D, so the qapp still works on hardware people own.
24//! * **Fail closed.** Rights-bounded render in a shared/civic standpoint defaults to **refuse**.
25
26use crate::gpu_context::OperationalMode;
27use crate::modalities::logic::deontic::{
28 compile_norm_quin, evaluate_deontic_contract, DeonticStatus, DeonticVerdict, OP_FORBID,
29 OP_PERMIT,
30};
31use crate::{q_hash, NQuin};
32
33/// Max views planned in one [`plan_qapp`] pass (stack-bounded, zero-heap).
34pub const MAX_QAPP_VIEWS: usize = 16;
35/// Max governance norms evaluated per rights-bounded check (stack-bounded, zero-heap).
36pub const MAX_GOV_NORMS: usize = 32;
37
38/// Property-path for a view-render action governed by a deontic norm.
39pub const P_VIEW_RENDER: u64 = q_hash("urn:qualia:authoring:render");
40/// Predicate stamp for an attestation `(attester) attests (manifold)`.
41pub const P_ATTESTS: u64 = q_hash("urn:qualia:authoring:attests");
42
43/// A view onto the manifold within a qapp.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ViewKind {
46 /// The 3D scene (the GPU `Volume3D` projection).
47 Scene3D,
48 /// A 2D pane (the `Plane2D` projection — the manifold's flat shadow).
49 Pane2D,
50}
51
52/// Sensitivity class — surfaces the §8 *rights-bounded context* primitive (not new doctrine).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum Sensitivity {
55 /// Renders anywhere.
56 Public,
57 /// Sensitive: a container **refuses** to render it in a shared/civic standpoint without consent.
58 RightsBounded,
59}
60
61/// One declared view in a qapp: a projection of `manifold`, with governance + budget annotations.
62#[derive(Debug, Clone, Copy)]
63pub struct QappView {
64 /// The manifold/tensor source id. The **same** id across views ⇒ "one manifold, many views".
65 pub manifold: u64,
66 pub kind: ViewKind,
67 pub sensitivity: Sensitivity,
68 /// If `true`, the view is withheld until an attestation for `manifold` is present.
69 pub requires_attestation: bool,
70}
71
72impl QappView {
73 /// A public, ungated view.
74 pub fn public(manifold: u64, kind: ViewKind) -> Self {
75 QappView {
76 manifold,
77 kind,
78 sensitivity: Sensitivity::Public,
79 requires_attestation: false,
80 }
81 }
82}
83
84/// The standpoint a qapp is being rendered into.
85#[derive(Debug, Clone, Copy)]
86pub struct RenderStandpoint {
87 pub id: u64,
88 /// A shared/civic view (multiple parties) vs the owner's private view. Sensitive content is
89 /// refused in a shared/civic view absent consent.
90 pub shared_civic: bool,
91}
92
93/// What the planner decides for a view (before any drawing happens).
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum ViewDisposition {
96 /// Render this view as the given kind.
97 Render(ViewKind),
98 /// A `Scene3D` view degraded to 2D under a constrained device budget (graceful, not a failure).
99 Collapsed2D,
100 /// Attestation-gated and not yet attested — withheld (wisdom-out-of-band).
101 WithheldUnattested,
102 /// Sensitive content in a shared/civic standpoint without consent — refused.
103 RefusedRightsBounded,
104}
105
106// ── budget (device tier → 3D capability) ─────────────────────────────────────────────────────────
107
108/// Whether the device tier supports the full 3D scene — delegates to the single-source budget rule
109/// [`OperationalMode::supports_3d`] (shared with the portal facade). `Eco`/`Reserve` **degrade
110/// 3D → 2D** — the affordability rail.
111#[inline]
112pub fn supports_3d(mode: OperationalMode) -> bool {
113 mode.supports_3d()
114}
115
116// ── attestation (wisdom-out-of-band) ─────────────────────────────────────────────────────────────
117
118/// Build an attestation quin `(attester) attests (manifold)` in a `frame`. Real DID-signature
119/// verification belongs to the identity/key-vault layer; this is the presence hook the gate checks.
120pub fn attestation_quin(attester: u64, manifold: u64, frame: u64) -> NQuin {
121 let mut q = NQuin {
122 subject: attester,
123 predicate: P_ATTESTS,
124 object: manifold,
125 context: frame,
126 metadata: 0,
127 parity: 0,
128 };
129 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context ^ q.metadata;
130 q
131}
132
133/// Whether some attestation in `attestations` ratifies this view's `manifold`.
134pub fn has_attestation(view: &QappView, attestations: &[NQuin]) -> bool {
135 attestations
136 .iter()
137 .any(|a| a.predicate == P_ATTESTS && a.object == view.manifold)
138}
139
140// ── rights-bounded context (deontic; fail-closed) ───────────────────────────────────────────────
141
142/// Build a civic-render consent norm `(standpoint) OPCODE render(manifold)` in a `frame`.
143pub fn view_render_norm(
144 standpoint: u64,
145 opcode: u8,
146 manifold: u64,
147 frame: u64,
148 expiry_unix32: u32,
149) -> NQuin {
150 compile_norm_quin(
151 standpoint,
152 opcode,
153 P_VIEW_RENDER,
154 manifold,
155 frame,
156 expiry_unix32,
157 false,
158 )
159}
160
161/// Whether sensitive content for `manifold` may render into `standpoint`.
162///
163/// * The owner's **private** view (`shared_civic == false`) ⇒ always permitted.
164/// * A **shared/civic** view ⇒ permitted only with an Active `PERMIT` for `(standpoint, manifold)`
165/// and no Active `FORBID`. **Fails closed** (no consent ⇒ refuse).
166pub fn rights_render_permitted(
167 standpoint: &RenderStandpoint,
168 manifold: u64,
169 gov_norms: &[NQuin],
170 now_unix: u32,
171) -> bool {
172 if !standpoint.shared_civic {
173 return true; // the owner's own private view
174 }
175 if gov_norms.len() > MAX_GOV_NORMS {
176 return false; // fail closed
177 }
178 let mut out = [DeonticVerdict::default(); MAX_GOV_NORMS];
179 let n = match evaluate_deontic_contract(gov_norms, now_unix, &mut out) {
180 Ok(n) => n,
181 Err(_) => return false, // fail closed
182 };
183 let mut permitted = false;
184 for v in &out[..n] {
185 if v.status != DeonticStatus::Active
186 || v.norm.subject != standpoint.id
187 || v.norm.object != manifold
188 {
189 continue;
190 }
191 match v.opcode {
192 OP_FORBID => return false, // an active prohibition always wins
193 OP_PERMIT => permitted = true,
194 _ => {}
195 }
196 }
197 permitted
198}
199
200// ── the planner ──────────────────────────────────────────────────────────────────────────────────
201
202/// Resolve one view's disposition, applying the gates in order: **attestation → rights-bounded →
203/// budget**. (Governance refusals take precedence over budget degradation — a refused view is not
204/// "degraded", it is withheld/refused.)
205pub fn plan_view(
206 view: &QappView,
207 standpoint: &RenderStandpoint,
208 mode: OperationalMode,
209 attestations: &[NQuin],
210 gov_norms: &[NQuin],
211 now_unix: u32,
212) -> ViewDisposition {
213 // 1) Attestation gate (wisdom-out-of-band): withhold until attested.
214 if view.requires_attestation && !has_attestation(view, attestations) {
215 return ViewDisposition::WithheldUnattested;
216 }
217 // 2) Rights-bounded context: refuse sensitive render in a shared/civic standpoint w/o consent.
218 if matches!(view.sensitivity, Sensitivity::RightsBounded)
219 && !rights_render_permitted(standpoint, view.manifold, gov_norms, now_unix)
220 {
221 return ViewDisposition::RefusedRightsBounded;
222 }
223 // 3) Budget: a 3D scene degrades to 2D on a constrained tier (affordability).
224 match view.kind {
225 ViewKind::Scene3D if !supports_3d(mode) => ViewDisposition::Collapsed2D,
226 k => ViewDisposition::Render(k),
227 }
228}
229
230/// Plan a whole qapp: write each view's disposition into `out`. Zero-heap (caller slices). Returns
231/// the number of dispositions written (`min(views.len(), out.len())`).
232#[allow(clippy::too_many_arguments)]
233pub fn plan_qapp(
234 views: &[QappView],
235 standpoint: &RenderStandpoint,
236 mode: OperationalMode,
237 attestations: &[NQuin],
238 gov_norms: &[NQuin],
239 now_unix: u32,
240 out: &mut [ViewDisposition],
241) -> usize {
242 let n = views.len().min(out.len());
243 for i in 0..n {
244 out[i] = plan_view(
245 &views[i],
246 standpoint,
247 mode,
248 attestations,
249 gov_norms,
250 now_unix,
251 );
252 }
253 n
254}
255
256/// A sample qapp: a `Scene3D` + a `Pane2D` over the **same** manifold — "one manifold, two views".
257pub fn sample_world_qapp(manifold: u64) -> [QappView; 2] {
258 [
259 QappView::public(manifold, ViewKind::Scene3D),
260 QappView::public(manifold, ViewKind::Pane2D),
261 ]
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn manifold() -> u64 {
269 q_hash("urn:qualia:manifold:demo")
270 }
271 fn owner() -> u64 {
272 q_hash("urn:qualia:standpoint:owner")
273 }
274 fn civic() -> RenderStandpoint {
275 RenderStandpoint {
276 id: q_hash("urn:qualia:standpoint:civic"),
277 shared_civic: true,
278 }
279 }
280 fn private() -> RenderStandpoint {
281 RenderStandpoint {
282 id: owner(),
283 shared_civic: false,
284 }
285 }
286 fn frame() -> u64 {
287 q_hash("urn:qualia:frame:app")
288 }
289
290 /// ACCEPTANCE (part 1): a 3D scene AND a 2D pane from ONE manifold, on a capable tier.
291 #[test]
292 fn one_manifold_two_views() {
293 let m = manifold();
294 let views = sample_world_qapp(m);
295 // both views point at the same manifold
296 assert_eq!(views[0].manifold, m);
297 assert_eq!(views[1].manifold, m);
298
299 let mut out = [ViewDisposition::Collapsed2D; MAX_QAPP_VIEWS];
300 let n = plan_qapp(
301 &views,
302 &private(),
303 OperationalMode::Full,
304 &[],
305 &[],
306 100,
307 &mut out,
308 );
309 assert_eq!(n, 2);
310 assert_eq!(out[0], ViewDisposition::Render(ViewKind::Scene3D));
311 assert_eq!(out[1], ViewDisposition::Render(ViewKind::Pane2D));
312 }
313
314 /// ACCEPTANCE (part 2): on a constrained device tier the 3D scene collapses to 2D; the 2D pane
315 /// is unaffected.
316 #[test]
317 fn budget_collapses_3d_to_2d() {
318 let views = sample_world_qapp(manifold());
319 for mode in [OperationalMode::Eco, OperationalMode::Reserve] {
320 let mut out = [ViewDisposition::Collapsed2D; MAX_QAPP_VIEWS];
321 plan_qapp(&views, &private(), mode, &[], &[], 100, &mut out);
322 assert_eq!(
323 out[0],
324 ViewDisposition::Collapsed2D,
325 "Scene3D should degrade under {mode:?}"
326 );
327 assert_eq!(out[1], ViewDisposition::Render(ViewKind::Pane2D));
328 }
329 // Full tier keeps the 3D scene.
330 let mut out = [ViewDisposition::Collapsed2D; MAX_QAPP_VIEWS];
331 plan_qapp(
332 &views,
333 &private(),
334 OperationalMode::Full,
335 &[],
336 &[],
337 100,
338 &mut out,
339 );
340 assert_eq!(out[0], ViewDisposition::Render(ViewKind::Scene3D));
341 }
342
343 /// ACCEPTANCE (part 3): a rights-bounded view is refused in a shared/civic standpoint without
344 /// consent, permitted with an Active PERMIT, and always shown in the owner's private view.
345 #[test]
346 fn rights_bounded_context_enforced() {
347 let m = manifold();
348 let sensitive = QappView {
349 manifold: m,
350 kind: ViewKind::Pane2D,
351 sensitivity: Sensitivity::RightsBounded,
352 requires_attestation: false,
353 };
354
355 // Civic, no consent → refused (fail closed).
356 let civ = civic();
357 assert_eq!(
358 plan_view(&sensitive, &civ, OperationalMode::Full, &[], &[], 100),
359 ViewDisposition::RefusedRightsBounded
360 );
361
362 // Civic, with an Active PERMIT for (civic standpoint, manifold) → rendered.
363 let permit = view_render_norm(civ.id, OP_PERMIT, m, frame(), 0);
364 assert_eq!(
365 plan_view(&sensitive, &civ, OperationalMode::Full, &[], &[permit], 100),
366 ViewDisposition::Render(ViewKind::Pane2D)
367 );
368
369 // Civic, with an Active FORBID → refused even if a permit is also present.
370 let forbid = view_render_norm(civ.id, OP_FORBID, m, frame(), 0);
371 assert_eq!(
372 plan_view(
373 &sensitive,
374 &civ,
375 OperationalMode::Full,
376 &[],
377 &[permit, forbid],
378 100
379 ),
380 ViewDisposition::RefusedRightsBounded
381 );
382
383 // Owner's private view → always rendered (no consent needed for one's own view).
384 assert_eq!(
385 plan_view(&sensitive, &private(), OperationalMode::Full, &[], &[], 100),
386 ViewDisposition::Render(ViewKind::Pane2D)
387 );
388 }
389
390 /// ACCEPTANCE (part 4): an attestation-gated view is withheld until attested (wisdom-out-of-band).
391 #[test]
392 fn attestation_gate_withholds_then_admits() {
393 let m = manifold();
394 let gated = QappView {
395 manifold: m,
396 kind: ViewKind::Scene3D,
397 sensitivity: Sensitivity::Public,
398 requires_attestation: true,
399 };
400
401 // No attestation → withheld.
402 assert_eq!(
403 plan_view(&gated, &private(), OperationalMode::Full, &[], &[], 100),
404 ViewDisposition::WithheldUnattested
405 );
406
407 // A matching attestation → rendered.
408 let attester = q_hash("did:example:auditor");
409 let att = attestation_quin(attester, m, frame());
410 assert!(has_attestation(&gated, &[att]));
411 assert_eq!(
412 plan_view(&gated, &private(), OperationalMode::Full, &[att], &[], 100),
413 ViewDisposition::Render(ViewKind::Scene3D)
414 );
415
416 // An attestation for a DIFFERENT manifold does not satisfy the gate.
417 let other = attestation_quin(attester, q_hash("urn:qualia:manifold:other"), frame());
418 assert_eq!(
419 plan_view(
420 &gated,
421 &private(),
422 OperationalMode::Full,
423 &[other],
424 &[],
425 100
426 ),
427 ViewDisposition::WithheldUnattested
428 );
429 }
430
431 /// Governance precedence: an attestation-gated, rights-bounded 3D view on a low tier resolves
432 /// the governance refusals *before* budget — withheld first, then (once attested) refused in
433 /// civic, then (once permitted) collapsed under budget.
434 #[test]
435 fn gates_compose_in_order() {
436 let m = manifold();
437 let v = QappView {
438 manifold: m,
439 kind: ViewKind::Scene3D,
440 sensitivity: Sensitivity::RightsBounded,
441 requires_attestation: true,
442 };
443 let civ = civic();
444 let att = attestation_quin(q_hash("did:example:auditor"), m, frame());
445 let permit = view_render_norm(civ.id, OP_PERMIT, m, frame(), 0);
446
447 // Unattested → withheld (attestation wins over everything).
448 assert_eq!(
449 plan_view(&v, &civ, OperationalMode::Eco, &[], &[permit], 100),
450 ViewDisposition::WithheldUnattested
451 );
452 // Attested but no consent in civic → refused.
453 assert_eq!(
454 plan_view(&v, &civ, OperationalMode::Eco, &[att], &[], 100),
455 ViewDisposition::RefusedRightsBounded
456 );
457 // Attested + consent, but low tier → collapsed to 2D.
458 assert_eq!(
459 plan_view(&v, &civ, OperationalMode::Eco, &[att], &[permit], 100),
460 ViewDisposition::Collapsed2D
461 );
462 // Attested + consent + full tier → the 3D scene renders.
463 assert_eq!(
464 plan_view(&v, &civ, OperationalMode::Full, &[att], &[permit], 100),
465 ViewDisposition::Render(ViewKind::Scene3D)
466 );
467 }
468}