Skip to main content

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}