qualia_core_db/render/place_time.rs
1//! Phase 3 — place / space / time binding for artefacts *(STELLAR §E step 3)*.
2//!
3//! An artefact is an **identifier** (`subject`); facts *about* it are companion NQuins that share
4//! that subject (see [`crate::render::assets::mesh_to_nquins`], where a mesh's `subject` is the
5//! `q_hash` of its asset IRI and its bbox/centroid/type are quins keyed by it). Phase 3 adds the
6//! **spatio-temporal** facts — *where* (a world/geo point + a jurisdiction frame) and *when* (a
7//! valid-time interval) — and demonstrates that the **same** artefact is queryable by the
8//! **inherited modality stack two ways**:
9//!
10//! * a **spatio-temporal** query — RCC-8 place containment + Allen interval relation
11//! ([`crate::modalities::spatio_temporal`]); and
12//! * a **deontic** query — a rights norm bound to the artefact's identity
13//! ([`crate::modalities::logic::deontic`]).
14//!
15//! One entity, one substrate, many modalities — this module is only a thin renderer-side *binding*
16//! (it packs the artefact's place/time into NQuin fields and shapes a footprint polygon); all the
17//! actual logic is delegated to the existing modalities. That is the Phase-3 rail: *spatio-temporal
18//! logic uses the inherited modality stack, not a bespoke engine.*
19//!
20//! The artefact's situatedness is one NQuin:
21//!
22//! | field | carries |
23//! |-------|---------|
24//! | `subject` | the artefact id (shared with its mesh facts and any norm about it) |
25//! | `predicate`| [`P_SITUATED_AT`] semantic stamp |
26//! | `object` | the artefact's `(x, y)` location, via [`pack_point`] (shared encoding with RCC-8) |
27//! | `context` | the jurisdiction / frame id |
28//! | `metadata` | the valid-time interval `[from, to]`, via [`pack_interval`] |
29
30use crate::modalities::logic::deontic::{
31 compile_norm_quin, evaluate_deontic_contract, DeonticStatus, DeonticVerdict, OP_FORBID,
32};
33use crate::modalities::spatio_temporal::{
34 evaluate_rcc8_points, evaluate_temporal, pack_point, unpack_point, Rcc8Relation, TemporalOp,
35};
36use crate::{q_hash, NQuin};
37
38/// Predicate stamp for an artefact's situatedness fact (place + valid-time).
39pub const P_SITUATED_AT: u64 = q_hash("urn:qualia:place:situatedAt");
40
41/// Property-path for a render/display norm (the action a deontic rule governs).
42pub const P_RENDER_DISPLAY: u64 = q_hash("urn:qualia:render:display");
43
44/// Max render norms evaluated in one [`render_permitted`] pass (stack-bounded, zero-heap).
45pub const MAX_RENDER_NORMS: usize = 32;
46
47// ── valid-time interval packing (one u64; whole seconds, i32 range) ──────────────────────────────
48
49/// Pack a valid-time interval `[start, end]` (whole seconds) into one `u64`: `start` in the high 32
50/// bits, `end` in the low 32 bits. Values are truncated to `i32` (≈ ±68 years around the epoch —
51/// honest demo range; a wider encoding is a file-format-v2 concern, STELLAR §C).
52#[inline]
53pub fn pack_interval(start: i64, end: i64) -> u64 {
54 let s = start as i32 as u32 as u64;
55 let e = end as i32 as u32 as u64;
56 (s << 32) | e
57}
58
59/// Inverse of [`pack_interval`] (sign-extends each 32-bit half back to `i64`).
60#[inline]
61pub fn unpack_interval(packed: u64) -> (i64, i64) {
62 let s = (packed >> 32) as u32 as i32 as i64;
63 let e = (packed & 0xFFFF_FFFF) as u32 as i32 as i64;
64 (s, e)
65}
66
67// ── situatedness fact ────────────────────────────────────────────────────────────────────────────
68
69/// Build the artefact's situatedness NQuin: the **same `subject`** as its mesh facts, carrying its
70/// world/geo location (`object`) and valid-time interval (`metadata`) in a jurisdiction frame
71/// (`context`). Parity is the XOR fold used throughout the engine.
72pub fn situate_artefact(
73 artefact_id: u64,
74 x: f64,
75 y: f64,
76 valid_from: i64,
77 valid_to: i64,
78 jurisdiction_frame: u64,
79) -> NQuin {
80 let object = pack_point(x, y);
81 let metadata = pack_interval(valid_from, valid_to);
82 let mut q = NQuin {
83 subject: artefact_id,
84 predicate: P_SITUATED_AT,
85 object,
86 context: jurisdiction_frame,
87 metadata,
88 parity: 0,
89 };
90 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context ^ q.metadata;
91 q
92}
93
94/// Recover the artefact's `(x, y)` location from its situatedness NQuin.
95#[inline]
96pub fn artefact_location(art: &NQuin) -> (f64, f64) {
97 unpack_point(art.object)
98}
99
100/// Recover the artefact's valid-time interval `[from, to]` from its situatedness NQuin.
101#[inline]
102pub fn artefact_interval(art: &NQuin) -> (i64, i64) {
103 unpack_interval(art.metadata)
104}
105
106// ── spatio-temporal query (over the artefact NQuin) ──────────────────────────────────────────────
107
108/// **Spatio-temporal query.** The RCC-8 topological relation of the artefact's footprint (a square
109/// of half-side `radius` centred on its location) to a `jurisdiction` polygon. Delegates to
110/// [`evaluate_rcc8_points`] — no bespoke geometry.
111pub fn place_relation(
112 art: &NQuin,
113 jurisdiction_id: u64,
114 jurisdiction_poly: &[(f64, f64)],
115 radius: f64,
116) -> Rcc8Relation {
117 let (cx, cy) = artefact_location(art);
118 let footprint = [
119 (cx - radius, cy - radius),
120 (cx + radius, cy - radius),
121 (cx + radius, cy + radius),
122 (cx - radius, cy + radius),
123 ];
124 evaluate_rcc8_points(art.subject, &footprint, jurisdiction_id, jurisdiction_poly)
125}
126
127/// True iff the artefact's footprint is **within** the jurisdiction (a proper part — tangential or
128/// not — or equal). A footprint that straddles the boundary (`PartiallyOverlapping`) is *not*
129/// within: a rights-bounded view does not render an artefact that pokes outside permitted space.
130pub fn situated_within(
131 art: &NQuin,
132 jurisdiction_id: u64,
133 jurisdiction_poly: &[(f64, f64)],
134 radius: f64,
135) -> bool {
136 matches!(
137 place_relation(art, jurisdiction_id, jurisdiction_poly, radius),
138 Rcc8Relation::NonTangentialProperPart
139 | Rcc8Relation::TangentiallyProperPart
140 | Rcc8Relation::Equal
141 )
142}
143
144/// **Temporal query.** The Allen relation `op` between the artefact's valid-time and a window.
145/// Delegates to [`evaluate_temporal`].
146pub fn time_relation(art: &NQuin, op: TemporalOp, window_start: i64, window_end: i64) -> bool {
147 let (s, e) = artefact_interval(art);
148 evaluate_temporal(op, s, e, window_start, window_end)
149}
150
151/// True iff the artefact's valid-time falls wholly **during** the window (Allen `During`).
152pub fn active_during(art: &NQuin, window_start: i64, window_end: i64) -> bool {
153 time_relation(art, TemporalOp::During, window_start, window_end)
154}
155
156// ── deontic query (over the same artefact's identity) ────────────────────────────────────────────
157
158/// Build a rights norm **bound to the artefact's identity**: `(party) OPCODE display(artefact)` in
159/// a `frame`, optionally expiring at `expiry_unix32` (`0` = no expiry). The action target
160/// (`object`) is the artefact's id, so this norm and the artefact's situatedness fact share the
161/// artefact identity — the deontic and spatio-temporal queries are over the *same* artefact.
162pub fn render_norm(
163 party: u64,
164 opcode: u8,
165 artefact_id: u64,
166 frame: u64,
167 expiry_unix32: u32,
168) -> NQuin {
169 compile_norm_quin(
170 party,
171 opcode,
172 P_RENDER_DISPLAY,
173 artefact_id,
174 frame,
175 expiry_unix32,
176 false,
177 )
178}
179
180/// **Deontic query.** Evaluate the render `norms` and return whether displaying this artefact is
181/// permitted: it is, unless some **Active `FORBID`** norm's action targets this artefact's id.
182///
183/// **Fails closed:** if the norm set exceeds [`MAX_RENDER_NORMS`] or cannot be evaluated, this
184/// returns `false` (deny) — a governance default, never a silent permit.
185pub fn render_permitted(art: &NQuin, norms: &[NQuin], now_unix: u32) -> bool {
186 if norms.len() > MAX_RENDER_NORMS {
187 return false; // fail closed: cannot evaluate the whole set in the bounded buffer
188 }
189 let mut out = [DeonticVerdict::default(); MAX_RENDER_NORMS];
190 let n = match evaluate_deontic_contract(norms, now_unix, &mut out) {
191 Ok(n) => n,
192 Err(_) => return false, // fail closed
193 };
194 let forbidden = out[..n].iter().any(|v| {
195 v.opcode == OP_FORBID && v.status == DeonticStatus::Active && v.norm.object == art.subject
196 });
197 !forbidden
198}
199
200// ── the integrative verdict (place + time + rights over ONE NQuin) ───────────────────────────────
201
202/// The structured outcome of querying ONE artefact NQuin across both modalities.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub struct SituatedVerdict {
205 /// RCC-8 relation of the artefact footprint to the jurisdiction.
206 pub place: Rcc8Relation,
207 /// Footprint is within the jurisdiction (spatio-temporal).
208 pub within_place: bool,
209 /// Valid-time is during the window (spatio-temporal).
210 pub within_time: bool,
211 /// No Active FORBID targets the artefact (deontic).
212 pub deontic_permits: bool,
213 /// Admit render iff situated in place AND time AND deontically permitted.
214 pub admit: bool,
215}
216
217/// Phase-3 acceptance in one call: given a **single artefact NQuin**, query it by the
218/// spatio-temporal modality (place + time) **and** the deontic modality (render norms bound to its
219/// identity), and combine. Render is admitted only when the artefact is situated within the
220/// jurisdiction, active during the window, **and** not under an Active prohibition.
221#[allow(clippy::too_many_arguments)]
222pub fn situated_render_verdict(
223 art: &NQuin,
224 jurisdiction_id: u64,
225 jurisdiction_poly: &[(f64, f64)],
226 footprint_radius: f64,
227 window_start: i64,
228 window_end: i64,
229 norms: &[NQuin],
230 now_unix: u32,
231) -> SituatedVerdict {
232 let place = place_relation(art, jurisdiction_id, jurisdiction_poly, footprint_radius);
233 let within_place = matches!(
234 place,
235 Rcc8Relation::NonTangentialProperPart
236 | Rcc8Relation::TangentiallyProperPart
237 | Rcc8Relation::Equal
238 );
239 let within_time = active_during(art, window_start, window_end);
240 let deontic_permits = render_permitted(art, norms, now_unix);
241 SituatedVerdict {
242 place,
243 within_place,
244 within_time,
245 deontic_permits,
246 admit: within_place && within_time && deontic_permits,
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::modalities::logic::deontic::OP_PERMIT;
254
255 // A square jurisdiction [0,10]^2 and an artefact id, reused across tests.
256 const JURIS_ID: u64 = 0xAB;
257 fn jurisdiction() -> [(f64, f64); 4] {
258 [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)]
259 }
260 fn art_id() -> u64 {
261 q_hash("urn:qualia:geometry:demo-cube")
262 }
263
264 #[test]
265 fn interval_and_location_round_trip() {
266 let (s, e) = unpack_interval(pack_interval(-1000, 2000));
267 assert_eq!((s, e), (-1000, 2000));
268
269 let art = situate_artefact(art_id(), 5.0, 5.0, 100, 200, JURIS_ID);
270 let (x, y) = artefact_location(&art);
271 assert!((x - 5.0).abs() < 1e-5 && (y - 5.0).abs() < 1e-5);
272 assert_eq!(artefact_interval(&art), (100, 200));
273 // parity is the canonical XOR fold
274 assert_eq!(
275 art.parity,
276 art.subject ^ art.predicate ^ art.object ^ art.context ^ art.metadata
277 );
278 }
279
280 #[test]
281 fn spatio_temporal_query_over_artefact() {
282 let poly = jurisdiction();
283 // Inside the jurisdiction, footprint strictly interior → NTPP / within.
284 let inside = situate_artefact(art_id(), 5.0, 5.0, 100, 200, JURIS_ID);
285 assert_eq!(
286 place_relation(&inside, JURIS_ID, &poly, 0.5),
287 Rcc8Relation::NonTangentialProperPart
288 );
289 assert!(situated_within(&inside, JURIS_ID, &poly, 0.5));
290 assert!(active_during(&inside, 0, 1000)); // [100,200] During [0,1000]
291
292 // Far outside → Disconnected / not within.
293 let outside = situate_artefact(art_id(), 50.0, 50.0, 100, 200, JURIS_ID);
294 assert_eq!(
295 place_relation(&outside, JURIS_ID, &poly, 0.5),
296 Rcc8Relation::Disconnected
297 );
298 assert!(!situated_within(&outside, JURIS_ID, &poly, 0.5));
299
300 // Straddling the boundary → PartiallyOverlapping / not within.
301 let straddle = situate_artefact(art_id(), 9.8, 5.0, 100, 200, JURIS_ID);
302 assert_eq!(
303 place_relation(&straddle, JURIS_ID, &poly, 0.5),
304 Rcc8Relation::PartiallyOverlapping
305 );
306 assert!(!situated_within(&straddle, JURIS_ID, &poly, 0.5));
307
308 // Outside the window → not during.
309 assert!(!active_during(&inside, 300, 1000)); // [100,200] is Before [300,1000]
310 }
311
312 #[test]
313 fn deontic_query_over_same_artefact() {
314 let art = situate_artefact(art_id(), 5.0, 5.0, 100, 200, JURIS_ID);
315 let viewer = q_hash("did:example:viewer");
316 let frame = q_hash("urn:qualia:frame:civic");
317
318 // No norms → permitted (a liberty).
319 assert!(render_permitted(&art, &[], 150));
320
321 // An Active FORBID targeting THIS artefact → denied.
322 let forbid = render_norm(viewer, OP_FORBID, art.subject, frame, 0);
323 assert!(!render_permitted(&art, &[forbid], 150));
324
325 // A PERMIT (no forbid) → permitted.
326 let permit = render_norm(viewer, OP_PERMIT, art.subject, frame, 0);
327 assert!(render_permitted(&art, &[permit], 150));
328
329 // An EXPIRED forbid (expiry in the past) is not Active → permitted again.
330 let expired = render_norm(viewer, OP_FORBID, art.subject, frame, 100);
331 assert!(render_permitted(&art, &[expired], 150));
332
333 // A forbid targeting a DIFFERENT artefact does not bind this one.
334 let other = render_norm(viewer, OP_FORBID, q_hash("urn:qualia:other"), frame, 0);
335 assert!(render_permitted(&art, &[other], 150));
336 }
337
338 /// PHASE-3 ACCEPTANCE: one artefact NQuin, queried by the spatio-temporal modality (place +
339 /// time) AND the deontic modality (a render norm bound to its identity) — over the *same* NQuin.
340 #[test]
341 fn same_nquin_two_modalities() {
342 let poly = jurisdiction();
343 let viewer = q_hash("did:example:viewer");
344 let frame = q_hash("urn:qualia:frame:civic");
345
346 // ONE artefact NQuin, situated at (5,5) during [100,200] in the civic frame.
347 let art = situate_artefact(art_id(), 5.0, 5.0, 100, 200, JURIS_ID);
348
349 // 1) Situated in place + time, no prohibition → admitted.
350 let v = situated_render_verdict(&art, JURIS_ID, &poly, 0.5, 0, 1000, &[], 150);
351 assert!(v.within_place && v.within_time && v.deontic_permits);
352 assert!(v.admit);
353
354 // 2) Same place/time, but an Active deontic FORBID over the SAME artefact → refused,
355 // even though it is spatio-temporally fine. (Deontic governs the render.)
356 let forbid = render_norm(viewer, OP_FORBID, art.subject, frame, 0);
357 let v2 = situated_render_verdict(&art, JURIS_ID, &poly, 0.5, 0, 1000, &[forbid], 150);
358 assert!(v2.within_place && v2.within_time);
359 assert!(!v2.deontic_permits);
360 assert!(!v2.admit);
361
362 // 3) Deontically permitted, but OUTSIDE the jurisdiction → refused on the spatio-temporal
363 // leg. (Place governs too — a rights-bounded view won't render out of permitted space.)
364 let outside = situate_artefact(art_id(), 50.0, 50.0, 100, 200, JURIS_ID);
365 let v3 = situated_render_verdict(&outside, JURIS_ID, &poly, 0.5, 0, 1000, &[], 150);
366 assert!(!v3.within_place);
367 assert!(v3.deontic_permits);
368 assert!(!v3.admit);
369
370 // 4) In place + permitted, but OUTSIDE the time window → refused on the temporal leg.
371 let v4 = situated_render_verdict(&art, JURIS_ID, &poly, 0.5, 300, 1000, &[], 150);
372 assert!(v4.within_place && v4.deontic_permits);
373 assert!(!v4.within_time);
374 assert!(!v4.admit);
375 }
376}