diff --git a/cockpit/src/OsintGraph.tsx b/cockpit/src/OsintGraph.tsx index 683a674b3..cc382efd2 100644 --- a/cockpit/src/OsintGraph.tsx +++ b/cockpit/src/OsintGraph.tsx @@ -77,25 +77,32 @@ const AX = { militaryUse: 0, civicUse: 1, airoRole: 2, mlTask: 3, purpose: 4, capacity: 5, currentStatus: 6, type: 7, output: 8, impact: 9, stakeholder: 10, }; -// McClelland motive — and its adjacency to Freud's developmental gradient. -// demand (need) and intent are INHERENT in the motive: the motive is the source -// of both reasoning axes. The POWER motive (nPow) isn't flat — it's a 4-level -// control-directionality scale (Freud's psychosexual stages), and it sits -// ADJACENT to airo:type: the actor role IS the power level. -// P1 Oral "consume from others to myself" → extraction (the consumed = AISubject) -// P2 Anal "control myself" → self-control (internal systems) -// P3 Phallic "control OTHERS" → AIDeployer/AIOperator (fields the tool) -// P4 Genital "empower OTHERS to control others"→ AIDeveloper/AIProvider/AISupplier (builds it) +// McClelland's nPow motive — with the 4-level developmental POWER gradient +// (Freud's psychosexual stages; taught in Falko Rheinberg's Potsdam motivation +// lectures). demand (need) and intent are INHERENT in the motive. The gradient +// is a control-directionality ladder, ADJACENT to airo:type — the actor role IS +// the power level. ALL FOUR levels are reachable: +// P1 Oral "I consume from others to myself" → AISubject (the consumed end) +// P2 Anal "I control myself" → AIOperator (controls the operation itself) +// P3 Phallic "I control others" → AIDeployer (fields the tool AT others) +// P4 Genital "I empower others/stakeholders to control" → AIDeveloper/AIProvider/AISupplier (builds/supplies it) // airo:type bits: 0=Subject(1) 1=Deployer(2) 2=Developer(4) 3=Provider(8) // 4=Operator(16) 5=Supplier(32). const MOTIVE = ['nPow', 'nAch', 'nAff']; -const POWER_LEVEL = ['—', 'P1·oral·consume', 'P2·anal·self', 'P3·phallic·control-others', 'P4·genital·empower']; -// Power level (0..4) read straight from the airo:type bitset — the adjacency. +const POWER_LEVEL = [ + '—', + 'P1·oral·consume from others', + 'P2·anal·control myself', + 'P3·phallic·control others', + 'P4·genital·empower others to control', +]; +// Power level (0..4) from the airo:type bitset — the adjacency, all four levels. // The boomerang (Deployer ∧ Subject) is P3 that has become P1's object. const powerOfAiro = (bits: number): number => { - if (bits & (4 | 8 | 32)) return 4; // Developer | Provider | Supplier — empower others - if (bits & (2 | 16)) return 3; // Deployer | Operator — control others - if (bits & 1) return 1; // Subject — the consumed + if (bits & (4 | 8 | 32)) return 4; // Developer | Provider | Supplier — empower others to control + if (bits & 2) return 3; // Deployer — control others (fields the tool at them) + if (bits & 16) return 2; // Operator — control the operation itself (self-control) + if (bits & 1) return 1; // Subject — consumed from return 0; }; // nAch / nAff still come from the intent/use LABELS (keyword heuristic); nPow is @@ -921,7 +928,14 @@ export function OsintGraph() { // builds to prove the harm the companies deny. The PERSON is the trait // (POWER_LEVEL from airo:type, else the McClelland motive) reasoned against it: // the divergence is trait-driven, not incidental to a "neutral" dual-use. - const lines = duModel.rows.slice(0, 40).map((r) => { + // P4 backdoor-networking platforms first (the Epstein archetype — power as + // brokerage between circles), then the per-capability causal chains. + const brokerLines = duModel.brokers.map((b) => ({ + text: `◆ P4 backdoor platform · ${b.label} — broker ${b.brk.toFixed(1)} (empowers others to control)`, + conf: 1, + survived: true, + })); + const chainLines = duModel.rows.slice(0, 40).map((r) => { const trait = r.pow ? POWER_LEVEL[r.pow] : r.motive >= 0 ? MOTIVE[r.motive] : '—'; return { text: `${r.label} → [mil ${r.mil}/civ ${r.civ}] → ${r.expl || '—'} ⟹ ${r.impl || '—'} │ ${trait}`, @@ -929,6 +943,7 @@ export function OsintGraph() { survived: r.divergence >= 0.5, }; }); + const lines = [...brokerLines, ...chainLines]; // Person→Situation attribution: how much of the high-divergence (the situational // intent→impact drift) is carried by a power trait (P3/P4 or nPow). High % = the // harm is trait-driven — the causal chain the "can't prove it's harmful" defense denies. @@ -1044,7 +1059,71 @@ export function OsintGraph() { nmeth.set(ax, m); } const nameOf = (ax: number, code: number) => nmeth.get(ax)?.get(code) ?? ''; - const powerOf = (i: number) => powerOfAiro(T[i * stride + AX.airoRole]); + // adjacency (undirected) + directed degree over the rendered entity edges — + // the SOCIAL power reading. The backdoor-networker (Epstein) has no AIRO role; + // his power is GRAPH POSITION: he brokers between otherwise-separate clusters. + const adj = new Map>(); + const outDeg = new Map(); + const inDeg = new Map(); + for (const e of view.semantic) { + let a = adj.get(e.s); + if (!a) { + a = new Set(); + adj.set(e.s, a); + } + a.add(e.t); + let b = adj.get(e.t); + if (!b) { + b = new Set(); + adj.set(e.t, b); + } + b.add(e.s); + outDeg.set(e.s, (outDeg.get(e.s) ?? 0) + 1); + inDeg.set(e.t, (inDeg.get(e.t) ?? 0) + 1); + } + // Burt structural-hole brokerage: degree × openness (neighbours that DON'T + // know each other). High = a node bridging distinct power circles — the + // backdoor-networking platform ("empower others to control", P4). + const brkCache = new Map(); + const brokerage = (i: number): number => { + const c = brkCache.get(i); + if (c !== undefined) return c; + const nb = adj.get(i); + let v = 0; + if (nb && nb.size >= 2) { + const arr = [...nb]; + const cap = Math.min(arr.length, 24); // bound the O(d²) pair scan + let links = 0; + let pairs = 0; + for (let x = 0; x < cap; x++) { + for (let y = x + 1; y < cap; y++) { + pairs += 1; + if (adj.get(arr[x])?.has(arr[y])) links += 1; + } + } + const cc = pairs ? links / pairs : 0; + v = nb.size * (1 - cc); + } + brkCache.set(i, v); + return v; + }; + // Person(2)/Stakeholder(1) power from graph POSITION; AI actors from airo:type. + const powerOfActor = (i: number): number => { + const brk = brokerage(i); + const o = outDeg.get(i) ?? 0; + const inn = inDeg.get(i) ?? 0; + if (brk >= 6 && (adj.get(i)?.size ?? 0) >= 4) return 4; // P4 backdoor platform (broker) + if (o > inn * 1.5 && o >= 3) return 3; // P3 controls others (out-edges) + if (inn > o * 1.5 && inn >= 2) return 1; // P1 consumed from (in-edges) + return 0; // affiliation / peripheral + }; + // combined: an AIRO actor role wins; else a social actor reads from position. + const powerOf = (i: number): number => { + const airo = T[i * stride + AX.airoRole]; + if (airo) return powerOfAiro(airo); + const c = soa.cls[i]; + return c === 1 || c === 2 ? powerOfActor(i) : 0; + }; const motiveOf = (i: number): number => { if (powerOf(i) > 0) return 0; // nPow is carried by the power level const txt = [ @@ -1165,7 +1244,15 @@ export function OsintGraph() { const d = capDiv.get(T[i * stride + AX.capacity]); if (d != null) nodeDiv.set(i, d); }); - return { rows, nodeDiv }; + // the P4 backdoor-networking platforms — Person/Stakeholder brokers, ranked by + // structural-hole brokerage. This is the Epstein archetype the model exists to + // explain: power as being the connective tissue between power circles. + const brokers = Array.from(view.touched) + .filter((i) => (soa.cls[i] === 1 || soa.cls[i] === 2) && powerOfActor(i) === 4) + .map((i) => ({ label: soa.labels[i] || `#${i}`, brk: brokerage(i) })) + .sort((a, b) => b.brk - a.brk) + .slice(0, 8); + return { rows, nodeDiv, brokers }; }, [soa, view]); const lensChip = (i: number): CSSProperties => ({ diff --git a/crates/cockpit-server/Cargo.toml b/crates/cockpit-server/Cargo.toml index c42d03b76..77207b079 100644 --- a/crates/cockpit-server/Cargo.toml +++ b/crates/cockpit-server/Cargo.toml @@ -23,6 +23,13 @@ futures-core.workspace = true serde.workspace = true serde_json = "1" +# Server-side templating — the OSINT ClassView card is rendered via a compile- +# time-checked askama template (the "XSLT" over the FieldMask projection, per +# OGAR docs/CLASSVIEW-FIELDVIEW-ASKAMA-BITMASK.md). Core crate only (render to +# String); the handler keeps its `Html` axum idiom, so no askama_axum ↔ +# axum version coupling. Matches woa-rs's askama pin. +askama = "0.12" + # ── The engine: lance-graph ────────────────────────────────────────── # Parser, DataFusion planner, LanceDB storage, blasgraph columnar, # semiring algebra, HHTL cascade. ALL queries route through here. diff --git a/crates/cockpit-server/src/main.rs b/crates/cockpit-server/src/main.rs index 26e8ffc97..19e50ef90 100644 --- a/crates/cockpit-server/src/main.rs +++ b/crates/cockpit-server/src/main.rs @@ -39,6 +39,7 @@ mod style_state; mod dto_bridge; mod codebook; mod mock_driver; +mod osint_classview; // ── Embed the Vite build at compile time ───────────────────────────────────── // The cockpit/ directory is built by `cd cockpit && npm run build` which @@ -222,6 +223,10 @@ async fn main() { // Pre-baked enriched OSINT SoA bytes — the 3D view (/osint3d) fetches // these and decodes each GUID → xyz client-side (no JSON). .route("/osint.soa", get(osint_soa_handler)) + // The canonical OSINT card (classid 0x0700) projected through a FieldMask — + // the Redmine-style ViewFilter, server-side (`?mask=`, omitted = FULL). + .route("/api/osint/card", get(osint_classview::osint_card_handler)) + .route("/api/osint/card.html", get(osint_classview::osint_card_html_handler)) // /body server-side HHTL LOD — POST camera → per-concept HhtlAction byte // (cascade over 1658 baked BlockBounds; native SIMD; client gates draw by it). .route("/api/body/lod", post(body_lod::body_lod_handler)) diff --git a/crates/cockpit-server/src/osint_classview.rs b/crates/cockpit-server/src/osint_classview.rs new file mode 100644 index 000000000..a8c014069 --- /dev/null +++ b/crates/cockpit-server/src/osint_classview.rs @@ -0,0 +1,303 @@ +//! The canonical **OSINT ClassView** — the `0x07XX` AIRO/AIwar domain. +//! +//! `0x07XX` is the operator-ratified canonical OSINT domain; the low byte (the +//! slot) is the owner's to assign. Two concepts are minted, mirroring the V3 SoA +//! bake (`data/osint-v3/`, `(APP_PREFIX 0x1000)<<16 | concept`): +//! +//! - [`OSINT_SYSTEM_CLASS`] `0x0700` — the **AI system** card: the 12 AIRO/VAIR +//! dims packed into GUID1's `6×(8:8)` tier cascade (HEEL `currentStatus:type`, +//! HIP `militaryUse:civicUse`, TWIG `MLTask:MLType`, LEAF `purpose:capacity`, +//! family `output:impact`, identity `stakeholder:airo_type`). +//! - [`OSINT_PERSON_CLASS`] `0x0701` — the **person** card: the 5 McClelland / +//! Rubicon dims from GUID2 (HEEL `stage:need`, HIP `receptor:rubicon`, +//! TWIG `motive`). This is the Epstein-archetype lens: motive (`nPow`/`nAch`/ +//! `nAff`) × Rubicon crossing × power receptor. +//! +//! This is the holy-grail schema for the OSINT domain: the ordered field card +//! whose *labels* live here (in the ClassView, above the SoA) while the *values* +//! live in the node's SoA tier bytes. The [`FieldMask`] is the Redmine-style +//! ViewFilter — a bitmask selecting which fields render; the askama template is +//! the XSLT that draws the projected rows (`class_view.rs` doctrine header). Bit +//! `i` == field `i` == the `i`-th tier byte, in the exact GUID order above. +//! +//! The `predicate_iri` prefix carries the **reasoning role** (need / offer / +//! intent / causality / person / …) so the two orthogonal axes (Demand +//! `offer⟷need`, Causality `intent⟷impact`) and the Person×Situation split are +//! read from the schema, not hard-coded. +//! +//! Canonical home is OGAR (`ogar-vocab`'s `osint_system` / `osint_person` +//! `Class` fns, lifted by `ogar-class-view::OgarClassView`); this q2-local impl +//! is the owner-authored definition kept byte-aligned with that mirror. It +//! follows the existing cockpit pattern of impl'ing a contract trait locally +//! (cf. `mock_driver.rs` impl'ing `CognitiveShaderDriver`). + +use std::sync::LazyLock; + +use askama::Template; +use axum::response::Html; +use axum::{extract::Query, Json}; +use lance_graph_contract::class_view::{ClassId, ClassView, FieldMask}; +use lance_graph_contract::ontology::{DisplayTemplate, FieldRef}; +use serde::Deserialize; + +/// classid `0x0700` — the OSINT **AI system** concept (GUID1 / AIRO dims). +pub const OSINT_SYSTEM_CLASS: ClassId = 0x0700; +/// classid `0x0701` — the OSINT **person** concept (GUID2 / McClelland dims). +pub const OSINT_PERSON_CLASS: ClassId = 0x0701; + +/// The AI-system card: 12 AIRO/VAIR fields in GUID1 `6×(8:8)` tier order. The +/// `predicate_iri` prefix is the reasoning **role**: `need` / `offer` (Demand +/// axis) · `intent` / `causality` (Causality axis) · `person` (actor role) · +/// `identity` / `state` / `relation` (context). +static OSINT_SYSTEM_FIELDS: LazyLock<[FieldRef; 12]> = LazyLock::new(|| { + [ + // HEEL hi:lo + FieldRef::new("aiwar:state/currentStatus", "currentStatus"), // 0 STATE + FieldRef::new("aiwar:identity/type", "type"), // 1 IDENTITY + // HIP hi:lo — the dual-use NEED pair + FieldRef::new("aiwar:need/militaryUse", "militaryUse"), // 2 NEED + FieldRef::new("aiwar:need/civicUse", "civicUse"), // 3 NEED + // TWIG hi:lo + FieldRef::new("aiwar:need/mlTask", "MLTask"), // 4 NEED (the task) + FieldRef::new("aiwar:offer/mlType", "MLType"), // 5 OFFER (the technique) + // LEAF hi:lo + FieldRef::new("aiwar:intent/purpose", "purpose:vair"), // 6 INTENT (explicit) + FieldRef::new("aiwar:offer/capacity", "capacity:airo"), // 7 OFFER + // family hi:lo + FieldRef::new("aiwar:offer/output", "output:airo"), // 8 OFFER + FieldRef::new("aiwar:causality/impact", "impact:vair"), // 9 CAUSALITY (implicit) + // identity hi:lo + FieldRef::new("aiwar:relation/stakeholder", "stakeholder"), // 10 RELATION (edge) + FieldRef::new("aiwar:person/airoRole", "airo:type"), // 11 PERSON (actor role) + ] +}); + +/// The person card: 5 McClelland / Rubicon fields in GUID2 tier order. Every +/// field is the `person` role — this is the Person side of Person×Situation +/// (the trait), where the system card carries the Situation (need/offer/impact). +static OSINT_PERSON_FIELDS: LazyLock<[FieldRef; 5]> = LazyLock::new(|| { + [ + // HEEL hi:lo + FieldRef::new("aiwar:person/stage", "stage"), // 0 Rubicon stage I..IV + FieldRef::new("aiwar:person/need", "need"), // 1 McClelland nPow/nAch/nAff + // HIP hi:lo + FieldRef::new("aiwar:person/receptor", "receptor"), // 2 power receptor + FieldRef::new("aiwar:person/rubicon", "rubicon"), // 3 Rubicon crossing + // TWIG hi + FieldRef::new("aiwar:person/motive", "motive"), // 4 dominant motive + ] +}); + +/// The owner-authored ClassView for the OSINT domain. `0x0700` resolves to the +/// AI-system card, `0x0701` to the person card; every other classid is the +/// zero-fallback empty shape. +pub struct OsintClassView; + +impl ClassView for OsintClassView { + fn fields(&self, class: ClassId) -> &[FieldRef] { + match class { + OSINT_SYSTEM_CLASS => &OSINT_SYSTEM_FIELDS[..], + OSINT_PERSON_CLASS => &OSINT_PERSON_FIELDS[..], + _ => &[], + } + } + + fn template(&self, _class: ClassId) -> DisplayTemplate { + DisplayTemplate::Card + } + + fn dolce_category_id(&self, _class: ClassId) -> u8 { + // OSINT node = a DOLCE endurant/object; the cache maps 0 → its default. + 0 + } +} + +/// The human-readable concept name for a known OSINT classid (for the card +/// header). Falls back to the hex id for anything else. +fn concept_name(class: ClassId) -> &'static str { + match class { + OSINT_SYSTEM_CLASS => "osint_system", + OSINT_PERSON_CLASS => "osint_person", + _ => "unknown", + } +} + +/// `?class=&mask=` — the ViewFilter. `class` omitted = the AI-system +/// card (`0x0700`); `mask` omitted = FULL. +#[derive(Deserialize)] +pub struct CardQuery { + class: Option, + mask: Option, +} + +impl CardQuery { + fn resolve(&self) -> (ClassId, FieldMask) { + let class = self.class.unwrap_or(OSINT_SYSTEM_CLASS); + let mask = self.mask.map(FieldMask).unwrap_or(FieldMask::FULL); + (class, mask) + } +} + +/// `GET /api/osint/card?class=&mask=` — project the card through the +/// FieldMask and return the surviving `(label, predicate)` rows. This is the +/// Redmine ERB ViewFilter, server-side: the mask selects the columns, the +/// ClassView resolves the labels, nothing is computed on the client. +pub async fn osint_card_handler(Query(q): Query) -> Json { + let (class, mask) = q.resolve(); + let cv = OsintClassView; + let rows: Vec = cv + .render_rows(class, mask) + .into_iter() + .map(|r| serde_json::json!({ "label": r.label, "predicate": r.predicate })) + .collect(); + Json(serde_json::json!({ + "classid": format!("0x{class:04x}"), + "concept": concept_name(class), + "mask": mask.0, + "field_count": cv.field_count(class), + "shown": rows.len(), + "rows": rows, + })) +} + +/// One projected card row — the askama template iterates these. `role` is the +/// reasoning role parsed from the predicate prefix (`aiwar:/`). +struct OsintCardRow { + role: String, + label: String, + predicate: String, +} + +/// The card view — a dumb askama loop over the mask-filtered rows (the "XSLT" +/// over the FieldMask projection). No per-field conditionals: the ViewFilter +/// already carved the row set in Rust. +#[derive(Template)] +#[template(path = "osint_card.html")] +struct OsintCardTemplate { + classid_hex: String, + concept: &'static str, + mask_hex: String, + shown: usize, + total: usize, + rows: Vec, +} + +/// `GET /api/osint/card.html?class=&mask=` — the same ViewFilter, +/// rendered server-side via the compile-time-checked askama template. The +/// *projection* (mask → rows) is identical to the JSON handler; askama is only +/// the XSLT. Each row shows its reasoning role (parsed from the predicate +/// prefix), the field label, and the predicate key. +pub async fn osint_card_html_handler(Query(q): Query) -> Html { + let (class, mask) = q.resolve(); + let cv = OsintClassView; + let rows: Vec = cv + .render_rows(class, mask) + .into_iter() + .map(|r| { + // predicate = "aiwar:/" — the reasoning role is the prefix. + let role = r + .predicate + .strip_prefix("aiwar:") + .and_then(|s| s.split('/').next()) + .unwrap_or("") + .to_string(); + OsintCardRow { + role, + label: r.label.to_string(), + predicate: r.predicate.to_string(), + } + }) + .collect(); + let tpl = OsintCardTemplate { + classid_hex: format!("0x{class:04x}"), + concept: concept_name(class), + mask_hex: format!("0x{:03x}", mask.0), + shown: rows.len(), + total: cv.field_count(class), + rows, + }; + // askama render is infallible for this static template; fall back to a + // terse error body rather than panicking in a request handler. + match tpl.render() { + Ok(body) => Html(body), + Err(e) => Html(format!("
osint card render error: {e}
")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn system_card_has_twelve_fields_in_tier_order() { + let cv = OsintClassView; + assert_eq!(cv.field_count(OSINT_SYSTEM_CLASS), 12); + // GUID1 tier order: HEEL currentStatus:type, HIP mil:civ, … + assert_eq!(cv.field_label(OSINT_SYSTEM_CLASS, 0), Some("currentStatus")); + assert_eq!(cv.field_label(OSINT_SYSTEM_CLASS, 2), Some("militaryUse")); + assert_eq!(cv.field_label(OSINT_SYSTEM_CLASS, 9), Some("impact:vair")); + assert_eq!(cv.field_label(OSINT_SYSTEM_CLASS, 11), Some("airo:type")); + // unknown class = zero-fallback empty shape. + assert_eq!(cv.field_count(0x0000), 0); + } + + #[test] + fn person_card_has_five_mcclelland_fields() { + let cv = OsintClassView; + assert_eq!(cv.field_count(OSINT_PERSON_CLASS), 5); + assert_eq!(cv.field_label(OSINT_PERSON_CLASS, 0), Some("stage")); + assert_eq!(cv.field_label(OSINT_PERSON_CLASS, 1), Some("need")); + assert_eq!(cv.field_label(OSINT_PERSON_CLASS, 4), Some("motive")); + } + + #[test] + fn field_mask_is_the_view_filter() { + let cv = OsintClassView; + // full mask → all 12 system rows. + assert_eq!( + cv.render_rows(OSINT_SYSTEM_CLASS, FieldMask::FULL).len(), + 12 + ); + // mask with only the Causality axis ends (intent bit 6 + impact bit 9). + let causal = FieldMask::EMPTY.with(6).with(9); + let rows = cv.render_rows(OSINT_SYSTEM_CLASS, causal); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].label, "purpose:vair"); + assert_eq!(rows[1].label, "impact:vair"); + } + + #[test] + fn html_card_renders_through_askama() { + // Smoke the askama path end-to-end for the person card: header + + // one row per selected field, role parsed from the predicate prefix. + let cv = OsintClassView; + let rows: Vec = cv + .render_rows(OSINT_PERSON_CLASS, FieldMask::FULL) + .into_iter() + .map(|r| OsintCardRow { + role: r + .predicate + .strip_prefix("aiwar:") + .and_then(|s| s.split('/').next()) + .unwrap_or("") + .to_string(), + label: r.label.to_string(), + predicate: r.predicate.to_string(), + }) + .collect(); + let tpl = OsintCardTemplate { + classid_hex: "0x0701".to_string(), + concept: "osint_person", + mask_hex: "0x01f".to_string(), + shown: rows.len(), + total: 5, + rows, + }; + let body = tpl.render().expect("askama render"); + assert!(body.contains("osint_person")); + assert!(body.contains("motive")); + assert!(body.contains("aiwar:person/motive")); + // dumb-loop template: every selected field is a row. + assert_eq!(body.matches("").count(), 5 + 1); // 5 rows + header row + } +} diff --git a/crates/cockpit-server/templates/osint_card.html b/crates/cockpit-server/templates/osint_card.html new file mode 100644 index 000000000..6b46e21ca --- /dev/null +++ b/crates/cockpit-server/templates/osint_card.html @@ -0,0 +1,19 @@ + + +OSINT {{ classid_hex }} card + +{# The mask carves, the loop renders. Zero per-field conditionals: the + ViewFilter (FieldMask) already selected the rows in Rust; this template is + the XSLT that draws the projection. See OGAR + docs/CLASSVIEW-FIELDVIEW-ASKAMA-BITMASK.md. #} +

OSINT · classid {{ classid_hex }} · {{ concept }} · FieldMask {{ mask_hex }} · {{ shown }} / {{ total }} fields shown

+ + +{% for row in rows %} +{% endfor %}
rolefieldpredicate
{{ row.role }}{{ row.label }}{{ row.predicate }}