From cd597c205f868ec0ef2b54c8005f69c32d6d93a6 Mon Sep 17 00:00:00 2001 From: "Claude (OSINT-V3 bake)" Date: Wed, 1 Jul 2026 15:29:33 +0000 Subject: [PATCH 1/4] osint: author the canonical 0x0700 ClassView (the AIRO/AIwar card) + 4-level power gradient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I own classid 0x0700 and its ClassView — so define it. The card that was returning `&[]` from OGAR now exists: - osint_classview.rs: OsintClassView impls lance_graph_contract::ClassView for 0x0700 with the 12-field card (bit i = ValueTenant position i), predicate_iri carrying the reasoning role (need/offer/intent/impact/person/state/identity/ relation). bit 11 = McClelland motive. Labels live in the ClassView (above the SoA), values in the entry — the Quartett card. - GET /api/osint/card?mask= — projects the card through a FieldMask (the Redmine ERB ViewFilter, server-side); omitted = FULL. render_rows(0x0700, mask) now returns the surviving (label, predicate) rows. Two unit tests cover the 12-field order + the mask filter. - Canonical home is OGAR (ogar-vocab osint ObjectView); this owner-authored q2-local impl works until mirrored upstream (same pattern as mock_driver.rs). Power gradient (Freud stages, Rheinberg's Potsdam lectures; McClelland nPow base) now has ALL FOUR levels reachable — powerOfAiro previously skipped P2: P1 oral 'I consume from others to myself' → AISubject P2 anal 'I control myself' → AIOperator (was missing) P3 phallic 'I control others' → AIDeployer P4 genital 'I empower others/stakeholders to control'→ AIDeveloper/Provider/Supplier Next: askama HTML template over render_rows + palette emits the FieldMask (moves the field render server-side → kills the vis-network repaint freeze). --- cockpit/src/OsintGraph.tsx | 35 +++-- crates/cockpit-server/src/main.rs | 4 + crates/cockpit-server/src/osint_classview.rs | 129 +++++++++++++++++++ 3 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 crates/cockpit-server/src/osint_classview.rs diff --git a/cockpit/src/OsintGraph.tsx b/cockpit/src/OsintGraph.tsx index 683a674b3..da53c1fc7 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 diff --git a/crates/cockpit-server/src/main.rs b/crates/cockpit-server/src/main.rs index 26e8ffc97..6d9fa6649 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,9 @@ 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)) // /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..9d78299ff --- /dev/null +++ b/crates/cockpit-server/src/osint_classview.rs @@ -0,0 +1,129 @@ +//! The canonical **OSINT ClassView** — classid `0x0700`, the AIRO/AIwar card. +//! +//! This is the holy-grail schema for the OSINT domain: the ordered 12-field card +//! whose *labels* live here (in the ClassView, above the SoA) while the *values* +//! live in the node's ValueTenant bytes. The `FieldMask` is the Redmine-style +//! ViewFilter — a bitmask selecting which fields render; an askama template is the +//! XSLT that draws the projected rows (`class_view.rs` doctrine header). +//! +//! bit `i` == ValueTenant position `i` (the N3 append-only discriminant). Order +//! MUST match `write_facet_tenant` in `osint_gotham.rs` (value bytes 1..=12) and +//! `FACET_AXES_UI`/`AX` in `cockpit/src/OsintGraph.tsx`. The `predicate_iri` +//! carries the **reasoning role** (need/offer/intent/impact/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` ObjectView); this q2-local impl +//! is the working owner-authored definition until it is mirrored upstream. 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 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 concept (the low u16 of the GUID classid). +pub const OSINT_CLASS: ClassId = 0x0700; + +/// The canonical OSINT card: 12 AIRO/VAIR fields in FieldMask-bit order. The +/// `predicate_iri` prefix is the reasoning **role**: +/// `need` / `offer` (Demand axis) · `intent` / `causality` (Causality axis) · +/// `person` (McClelland/Freud trait) · `identity` / `state` / `relation` (context). +static OSINT_FIELDS: LazyLock<[FieldRef; 12]> = LazyLock::new(|| { + [ + FieldRef::new("aiwar:need/militaryUse", "militaryUse"), // 0 NEED + FieldRef::new("aiwar:need/civicUse", "civicUse"), // 1 NEED + FieldRef::new("aiwar:person/airoRole", "airo:type"), // 2 PERSON (power P1..P4) + FieldRef::new("aiwar:need/mlTask", "MLTask"), // 3 NEED + FieldRef::new("aiwar:intent/purpose", "purpose:vair"), // 4 INTENT (explicit) + FieldRef::new("aiwar:offer/capacity", "capacity:airo"), // 5 OFFER + FieldRef::new("aiwar:state/currentStatus", "currentStatus"), // 6 STATE + FieldRef::new("aiwar:identity/type", "type"), // 7 IDENTITY + FieldRef::new("aiwar:offer/output", "output:airo"), // 8 OFFER + FieldRef::new("aiwar:causality/impact", "impact:vair"), // 9 CAUSALITY (implicit) + FieldRef::new("aiwar:relation/stakeholder", "stakeholder"), // 10 RELATION (edge) + FieldRef::new("aiwar:person/motive", "motive"), // 11 PERSON (McClelland nPow/nAch/nAff) + ] +}); + +/// The owner-authored ClassView for classid `0x0700`. Only `0x0700` resolves to +/// the card; every other classid is the zero-fallback empty shape. +pub struct OsintClassView; + +impl ClassView for OsintClassView { + fn fields(&self, class: ClassId) -> &[FieldRef] { + if class == OSINT_CLASS { + &OSINT_FIELDS[..] + } else { + &[] + } + } + + 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 + } +} + +/// `?mask=` — the ViewFilter bitmask (bit i = show field i). Omitted = FULL. +#[derive(Deserialize)] +pub struct CardQuery { + mask: Option, +} + +/// `GET /api/osint/card?mask=` — project the `0x0700` 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 mask = q.mask.map(FieldMask).unwrap_or(FieldMask::FULL); + let cv = OsintClassView; + let rows: Vec = cv + .render_rows(OSINT_CLASS, mask) + .into_iter() + .map(|r| serde_json::json!({ "label": r.label, "predicate": r.predicate })) + .collect(); + Json(serde_json::json!({ + "classid": format!("0x{OSINT_CLASS:04x}"), + "mask": mask.0, + "field_count": cv.field_count(OSINT_CLASS), + "shown": rows.len(), + "rows": rows, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn card_has_twelve_fields_in_bit_order() { + let cv = OsintClassView; + assert_eq!(cv.field_count(OSINT_CLASS), 12); + assert_eq!(cv.field_label(OSINT_CLASS, 0), Some("militaryUse")); + assert_eq!(cv.field_label(OSINT_CLASS, 9), Some("impact:vair")); + assert_eq!(cv.field_label(OSINT_CLASS, 11), Some("motive")); + // unknown class = zero-fallback empty shape. + assert_eq!(cv.field_count(0x0000), 0); + } + + #[test] + fn field_mask_is_the_view_filter() { + let cv = OsintClassView; + // full mask → all 12 rows. + assert_eq!(cv.render_rows(OSINT_CLASS, FieldMask::FULL).len(), 12); + // mask with only the Causality axis ends (intent bit 4 + impact bit 9). + let causal = FieldMask::EMPTY.with(4).with(9); + let rows = cv.render_rows(OSINT_CLASS, causal); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].label, "purpose:vair"); + assert_eq!(rows[1].label, "impact:vair"); + } +} From d5f763337ed832566311afdbe9e69861af6b68d5 Mon Sep 17 00:00:00 2001 From: "Claude (OSINT-V3 bake)" Date: Wed, 1 Jul 2026 15:53:25 +0000 Subject: [PATCH 2/4] =?UTF-8?q?osint:=20powerOfActor=20=E2=80=94=20catch?= =?UTF-8?q?=20the=20Epstein=20archetype=20(P4=20broker)=20via=20graph=20po?= =?UTF-8?q?sition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The power model exists to explain the desire to be in power circles (Epstein the archetype): control others / get something / fill a need / be a platform for backdoor networking. The last — 'empower others/stakeholders to control others' (P4) — is the broker, and his power is NOT an AIRO actor role: it's GRAPH POSITION. powerOfAiro alone read him as P0. Add powerOfActor for Person(2)/Stakeholder(1) nodes, from graph structure over the rendered entity edges: - P4 backdoor platform = Burt structural-hole brokerage (degree × openness): high degree bridging neighbours who don't know each other — the connective tissue between otherwise-separate power circles. - P3 controls others = out-edge dominance. - P1 consumed from = in-edge dominance. - else affiliation/peripheral. powerOf(i) now combines: an AIRO role wins (AI-system actor); else a social actor reads from position. The divergence readout lists the top P4 brokers first — surfacing the backdoor-networker the model was built to explain. Brokerage is cached + the O(d²) pair scan is capped at 24 neighbours. --- cockpit/src/OsintGraph.tsx | 86 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/cockpit/src/OsintGraph.tsx b/cockpit/src/OsintGraph.tsx index da53c1fc7..cc382efd2 100644 --- a/cockpit/src/OsintGraph.tsx +++ b/cockpit/src/OsintGraph.tsx @@ -928,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}`, @@ -936,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. @@ -1051,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 = [ @@ -1172,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 => ({ From 6f19621992b52de75e17a7ce939d81b0fdcb8962 Mon Sep 17 00:00:00 2001 From: "Claude (OSINT-V3 bake)" Date: Wed, 1 Jul 2026 16:25:41 +0000 Subject: [PATCH 3/4] osint: server-side FieldMask card render (/api/osint/card.html) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Redmine ERB ViewFilter, rendered server-side in the codebase's own Html idiom (no template engine exists in the workspace; adding askama blind on edition-2024 would risk the whole binary). render_rows is template- agnostic, so this is a drop-in swap for an askama template once a version is pinned — the projection (mask → rows) is identical either way. GET /api/osint/card.html?mask= renders the 0x0700 card as an HTML table: role (from the predicate prefix) · field · predicate, FieldMask-filtered. --- crates/cockpit-server/src/main.rs | 1 + crates/cockpit-server/src/osint_classview.rs | 39 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/cockpit-server/src/main.rs b/crates/cockpit-server/src/main.rs index 6d9fa6649..19e50ef90 100644 --- a/crates/cockpit-server/src/main.rs +++ b/crates/cockpit-server/src/main.rs @@ -226,6 +226,7 @@ async fn main() { // 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 index 9d78299ff..596e4e032 100644 --- a/crates/cockpit-server/src/osint_classview.rs +++ b/crates/cockpit-server/src/osint_classview.rs @@ -20,6 +20,7 @@ use std::sync::LazyLock; +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}; @@ -99,6 +100,44 @@ pub async fn osint_card_handler(Query(q): Query) -> Json` — the same ViewFilter, rendered +/// server-side as HTML (the Redmine ERB view, in this codebase's `Html` +/// idiom). `render_rows` is template-agnostic, so this is a drop-in swap for an +/// askama template once a version is pinned; the *projection* (mask → rows) is +/// identical either way. 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 mask = q.mask.map(FieldMask).unwrap_or(FieldMask::FULL); + let cv = OsintClassView; + let rows = cv.render_rows(OSINT_CLASS, mask); + let mut body = String::new(); + for r in &rows { + // predicate = "aiwar:/" — the reasoning role is the prefix. + let role = r + .predicate + .strip_prefix("aiwar:") + .and_then(|s| s.split('/').next()) + .unwrap_or(""); + body.push_str(&format!( + "{role}{}{}", + r.label, r.predicate + )); + } + Html(format!( + "OSINT 0x0700 card\ +\ +

OSINT · classid 0x0700 · FieldMask 0x{:03x} · {} / {} fields shown

\ +{}
rolefieldpredicate
", + mask.0, + rows.len(), + cv.field_count(OSINT_CLASS), + body + )) +} + #[cfg(test)] mod tests { use super::*; From dd1595f6cbea97e251d747878b3004f15993032e Mon Sep 17 00:00:00 2001 From: "Claude (OSINT-V3 bake)" Date: Wed, 1 Jul 2026 16:55:03 +0000 Subject: [PATCH 4/4] osint: real askama card render + align ClassView to the V3 SoA schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (b) Add askama 0.12 (core, render-to-String — keeps the Html axum idiom, no askama_axum↔axum coupling; matches woa-rs's pin). The card.html handler now renders through a compile-time-checked askama template (templates/osint_card.html): the FieldMask carves the rows in Rust, the template is a dumb {% for %} loop with zero per-field conditionals — the "XSLT over the projection" pattern from OGAR's CLASSVIEW-FIELDVIEW-ASKAMA- BITMASK doctrine. Default HTML escaping (XSS-safe). Align the ClassView to the V3 SoA bake (data/osint-v3/) and the OGAR mirror (ogar-vocab osint_system/osint_person), so bake ↔ reasoning ↔ OGAR canon agree. 07xx is the operator-ratified canonical OSINT domain; the low u16 is the frozen concept, the APP_PREFIX (0x1000, V3 format signal) is the render half. Two concepts, matching the two GUIDs of a baked node: - OSINT_SYSTEM_CLASS 0x0700 — 12 AIRO/VAIR dims in GUID1 6×(8:8) tier order (currentStatus, type, militaryUse, civicUse, MLTask, MLType, purpose, capacity, output, impact, stakeholder, airo:type). predicate_iri carries the reasoning role (need/offer/intent/causality/person/…). - OSINT_PERSON_CLASS 0x0701 — 5 McClelland/Rubicon dims from GUID2 (stage, need, receptor, rubicon, motive): the Epstein-archetype motive lens. /api/osint/card[.html] now take an optional ?class= (defaults to the system card). Unit tests cover both cards + the askama render path. Note: cockpit-server (deno_core fork + lance-graph + ndarray tree) is too heavy to compile in-session; Railway is the build check. Co-Authored-By: Claude --- crates/cockpit-server/Cargo.toml | 7 + crates/cockpit-server/src/osint_classview.rs | 303 +++++++++++++----- .../cockpit-server/templates/osint_card.html | 19 ++ 3 files changed, 245 insertions(+), 84 deletions(-) create mode 100644 crates/cockpit-server/templates/osint_card.html 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/osint_classview.rs b/crates/cockpit-server/src/osint_classview.rs index 596e4e032..a8c014069 100644 --- a/crates/cockpit-server/src/osint_classview.rs +++ b/crates/cockpit-server/src/osint_classview.rs @@ -1,65 +1,104 @@ -//! The canonical **OSINT ClassView** — classid `0x0700`, the AIRO/AIwar card. +//! The canonical **OSINT ClassView** — the `0x07XX` AIRO/AIwar domain. //! -//! This is the holy-grail schema for the OSINT domain: the ordered 12-field card +//! `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 ValueTenant bytes. The `FieldMask` is the Redmine-style -//! ViewFilter — a bitmask selecting which fields render; an askama template is the -//! XSLT that draws the projected rows (`class_view.rs` doctrine header). +//! 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. //! -//! bit `i` == ValueTenant position `i` (the N3 append-only discriminant). Order -//! MUST match `write_facet_tenant` in `osint_gotham.rs` (value bytes 1..=12) and -//! `FACET_AXES_UI`/`AX` in `cockpit/src/OsintGraph.tsx`. The `predicate_iri` -//! carries the **reasoning role** (need/offer/intent/impact/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. +//! 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` ObjectView); this q2-local impl -//! is the working owner-authored definition until it is mirrored upstream. It +//! 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 concept (the low u16 of the GUID classid). -pub const OSINT_CLASS: ClassId = 0x0700; +/// 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 canonical OSINT card: 12 AIRO/VAIR fields in FieldMask-bit order. The -/// `predicate_iri` prefix is the reasoning **role**: -/// `need` / `offer` (Demand axis) · `intent` / `causality` (Causality axis) · -/// `person` (McClelland/Freud trait) · `identity` / `state` / `relation` (context). -static OSINT_FIELDS: LazyLock<[FieldRef; 12]> = LazyLock::new(|| { +/// 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(|| { [ - FieldRef::new("aiwar:need/militaryUse", "militaryUse"), // 0 NEED - FieldRef::new("aiwar:need/civicUse", "civicUse"), // 1 NEED - FieldRef::new("aiwar:person/airoRole", "airo:type"), // 2 PERSON (power P1..P4) - FieldRef::new("aiwar:need/mlTask", "MLTask"), // 3 NEED - FieldRef::new("aiwar:intent/purpose", "purpose:vair"), // 4 INTENT (explicit) - FieldRef::new("aiwar:offer/capacity", "capacity:airo"), // 5 OFFER - FieldRef::new("aiwar:state/currentStatus", "currentStatus"), // 6 STATE - FieldRef::new("aiwar:identity/type", "type"), // 7 IDENTITY + // 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/motive", "motive"), // 11 PERSON (McClelland nPow/nAch/nAff) + FieldRef::new("aiwar:person/airoRole", "airo:type"), // 11 PERSON (actor role) ] }); -/// The owner-authored ClassView for classid `0x0700`. Only `0x0700` resolves to -/// the card; every other classid is the zero-fallback empty shape. +/// 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] { - if class == OSINT_CLASS { - &OSINT_FIELDS[..] - } else { - &[] + match class { + OSINT_SYSTEM_CLASS => &OSINT_SYSTEM_FIELDS[..], + OSINT_PERSON_CLASS => &OSINT_PERSON_FIELDS[..], + _ => &[], } } @@ -73,69 +112,116 @@ impl ClassView for OsintClassView { } } -/// `?mask=` — the ViewFilter bitmask (bit i = show field i). Omitted = FULL. +/// 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, } -/// `GET /api/osint/card?mask=` — project the `0x0700` card through the +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 mask = q.mask.map(FieldMask).unwrap_or(FieldMask::FULL); + let (class, mask) = q.resolve(); let cv = OsintClassView; let rows: Vec = cv - .render_rows(OSINT_CLASS, mask) + .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{OSINT_CLASS:04x}"), + "classid": format!("0x{class:04x}"), + "concept": concept_name(class), "mask": mask.0, - "field_count": cv.field_count(OSINT_CLASS), + "field_count": cv.field_count(class), "shown": rows.len(), "rows": rows, })) } -/// `GET /api/osint/card.html?mask=` — the same ViewFilter, rendered -/// server-side as HTML (the Redmine ERB view, in this codebase's `Html` -/// idiom). `render_rows` is template-agnostic, so this is a drop-in swap for an -/// askama template once a version is pinned; the *projection* (mask → rows) is -/// identical either way. Each row shows its reasoning role (parsed from the -/// predicate prefix), the field label, and the predicate key. +/// 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 mask = q.mask.map(FieldMask).unwrap_or(FieldMask::FULL); + let (class, mask) = q.resolve(); let cv = OsintClassView; - let rows = cv.render_rows(OSINT_CLASS, mask); - let mut body = String::new(); - for r in &rows { - // predicate = "aiwar:/" — the reasoning role is the prefix. - let role = r - .predicate - .strip_prefix("aiwar:") - .and_then(|s| s.split('/').next()) - .unwrap_or(""); - body.push_str(&format!( - "{role}{}{}", - r.label, r.predicate - )); + 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}
")), } - Html(format!( - "OSINT 0x0700 card\ -\ -

OSINT · classid 0x0700 · FieldMask 0x{:03x} · {} / {} fields shown

\ -{}
rolefieldpredicate
", - mask.0, - rows.len(), - cv.field_count(OSINT_CLASS), - body - )) } #[cfg(test)] @@ -143,26 +229,75 @@ mod tests { use super::*; #[test] - fn card_has_twelve_fields_in_bit_order() { + fn system_card_has_twelve_fields_in_tier_order() { let cv = OsintClassView; - assert_eq!(cv.field_count(OSINT_CLASS), 12); - assert_eq!(cv.field_label(OSINT_CLASS, 0), Some("militaryUse")); - assert_eq!(cv.field_label(OSINT_CLASS, 9), Some("impact:vair")); - assert_eq!(cv.field_label(OSINT_CLASS, 11), Some("motive")); + 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 rows. - assert_eq!(cv.render_rows(OSINT_CLASS, FieldMask::FULL).len(), 12); - // mask with only the Causality axis ends (intent bit 4 + impact bit 9). - let causal = FieldMask::EMPTY.with(4).with(9); - let rows = cv.render_rows(OSINT_CLASS, causal); + // 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 }}