Skip to content

Commit dd1595f

Browse files
AdaWorldAPIclaude
andcommitted
osint: real askama card render + align ClassView to the V3 SoA schema
(b) Add askama 0.12 (core, render-to-String — keeps the Html<String> 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=<id> (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 <noreply@anthropic.com>
1 parent 6f19621 commit dd1595f

3 files changed

Lines changed: 245 additions & 84 deletions

File tree

crates/cockpit-server/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ futures-core.workspace = true
2323
serde.workspace = true
2424
serde_json = "1"
2525

26+
# Server-side templating — the OSINT ClassView card is rendered via a compile-
27+
# time-checked askama template (the "XSLT" over the FieldMask projection, per
28+
# OGAR docs/CLASSVIEW-FIELDVIEW-ASKAMA-BITMASK.md). Core crate only (render to
29+
# String); the handler keeps its `Html<String>` axum idiom, so no askama_axum ↔
30+
# axum version coupling. Matches woa-rs's askama pin.
31+
askama = "0.12"
32+
2633
# ── The engine: lance-graph ──────────────────────────────────────────
2734
# Parser, DataFusion planner, LanceDB storage, blasgraph columnar,
2835
# semiring algebra, HHTL cascade. ALL queries route through here.
Lines changed: 219 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,104 @@
1-
//! The canonical **OSINT ClassView** — classid `0x0700`, the AIRO/AIwar card.
1+
//! The canonical **OSINT ClassView** — the `0x07XX` AIRO/AIwar domain.
22
//!
3-
//! This is the holy-grail schema for the OSINT domain: the ordered 12-field card
3+
//! `0x07XX` is the operator-ratified canonical OSINT domain; the low byte (the
4+
//! slot) is the owner's to assign. Two concepts are minted, mirroring the V3 SoA
5+
//! bake (`data/osint-v3/`, `(APP_PREFIX 0x1000)<<16 | concept`):
6+
//!
7+
//! - [`OSINT_SYSTEM_CLASS`] `0x0700` — the **AI system** card: the 12 AIRO/VAIR
8+
//! dims packed into GUID1's `6×(8:8)` tier cascade (HEEL `currentStatus:type`,
9+
//! HIP `militaryUse:civicUse`, TWIG `MLTask:MLType`, LEAF `purpose:capacity`,
10+
//! family `output:impact`, identity `stakeholder:airo_type`).
11+
//! - [`OSINT_PERSON_CLASS`] `0x0701` — the **person** card: the 5 McClelland /
12+
//! Rubicon dims from GUID2 (HEEL `stage:need`, HIP `receptor:rubicon`,
13+
//! TWIG `motive`). This is the Epstein-archetype lens: motive (`nPow`/`nAch`/
14+
//! `nAff`) × Rubicon crossing × power receptor.
15+
//!
16+
//! This is the holy-grail schema for the OSINT domain: the ordered field card
417
//! whose *labels* live here (in the ClassView, above the SoA) while the *values*
5-
//! live in the node's ValueTenant bytes. The `FieldMask` is the Redmine-style
6-
//! ViewFilter — a bitmask selecting which fields render; an askama template is the
7-
//! XSLT that draws the projected rows (`class_view.rs` doctrine header).
18+
//! live in the node's SoA tier bytes. The [`FieldMask`] is the Redmine-style
19+
//! ViewFilter — a bitmask selecting which fields render; the askama template is
20+
//! the XSLT that draws the projected rows (`class_view.rs` doctrine header). Bit
21+
//! `i` == field `i` == the `i`-th tier byte, in the exact GUID order above.
822
//!
9-
//! bit `i` == ValueTenant position `i` (the N3 append-only discriminant). Order
10-
//! MUST match `write_facet_tenant` in `osint_gotham.rs` (value bytes 1..=12) and
11-
//! `FACET_AXES_UI`/`AX` in `cockpit/src/OsintGraph.tsx`. The `predicate_iri`
12-
//! carries the **reasoning role** (need/offer/intent/impact/person/…) so the two
13-
//! orthogonal axes (Demand `offer⟷need`, Causality `intent⟷impact`) and the
14-
//! Person×Situation split are read from the schema, not hard-coded.
23+
//! The `predicate_iri` prefix carries the **reasoning role** (need / offer /
24+
//! intent / causality / person / …) so the two orthogonal axes (Demand
25+
//! `offer⟷need`, Causality `intent⟷impact`) and the Person×Situation split are
26+
//! read from the schema, not hard-coded.
1527
//!
16-
//! Canonical home is OGAR (`ogar-vocab`'s `osint` ObjectView); this q2-local impl
17-
//! is the working owner-authored definition until it is mirrored upstream. It
28+
//! Canonical home is OGAR (`ogar-vocab`'s `osint_system` / `osint_person`
29+
//! `Class` fns, lifted by `ogar-class-view::OgarClassView`); this q2-local impl
30+
//! is the owner-authored definition kept byte-aligned with that mirror. It
1831
//! follows the existing cockpit pattern of impl'ing a contract trait locally
1932
//! (cf. `mock_driver.rs` impl'ing `CognitiveShaderDriver`).
2033
2134
use std::sync::LazyLock;
2235

36+
use askama::Template;
2337
use axum::response::Html;
2438
use axum::{extract::Query, Json};
2539
use lance_graph_contract::class_view::{ClassId, ClassView, FieldMask};
2640
use lance_graph_contract::ontology::{DisplayTemplate, FieldRef};
2741
use serde::Deserialize;
2842

29-
/// classid `0x0700` — the OSINT concept (the low u16 of the GUID classid).
30-
pub const OSINT_CLASS: ClassId = 0x0700;
43+
/// classid `0x0700` — the OSINT **AI system** concept (GUID1 / AIRO dims).
44+
pub const OSINT_SYSTEM_CLASS: ClassId = 0x0700;
45+
/// classid `0x0701` — the OSINT **person** concept (GUID2 / McClelland dims).
46+
pub const OSINT_PERSON_CLASS: ClassId = 0x0701;
3147

32-
/// The canonical OSINT card: 12 AIRO/VAIR fields in FieldMask-bit order. The
33-
/// `predicate_iri` prefix is the reasoning **role**:
34-
/// `need` / `offer` (Demand axis) · `intent` / `causality` (Causality axis) ·
35-
/// `person` (McClelland/Freud trait) · `identity` / `state` / `relation` (context).
36-
static OSINT_FIELDS: LazyLock<[FieldRef; 12]> = LazyLock::new(|| {
48+
/// The AI-system card: 12 AIRO/VAIR fields in GUID1 `6×(8:8)` tier order. The
49+
/// `predicate_iri` prefix is the reasoning **role**: `need` / `offer` (Demand
50+
/// axis) · `intent` / `causality` (Causality axis) · `person` (actor role) ·
51+
/// `identity` / `state` / `relation` (context).
52+
static OSINT_SYSTEM_FIELDS: LazyLock<[FieldRef; 12]> = LazyLock::new(|| {
3753
[
38-
FieldRef::new("aiwar:need/militaryUse", "militaryUse"), // 0 NEED
39-
FieldRef::new("aiwar:need/civicUse", "civicUse"), // 1 NEED
40-
FieldRef::new("aiwar:person/airoRole", "airo:type"), // 2 PERSON (power P1..P4)
41-
FieldRef::new("aiwar:need/mlTask", "MLTask"), // 3 NEED
42-
FieldRef::new("aiwar:intent/purpose", "purpose:vair"), // 4 INTENT (explicit)
43-
FieldRef::new("aiwar:offer/capacity", "capacity:airo"), // 5 OFFER
44-
FieldRef::new("aiwar:state/currentStatus", "currentStatus"), // 6 STATE
45-
FieldRef::new("aiwar:identity/type", "type"), // 7 IDENTITY
54+
// HEEL hi:lo
55+
FieldRef::new("aiwar:state/currentStatus", "currentStatus"), // 0 STATE
56+
FieldRef::new("aiwar:identity/type", "type"), // 1 IDENTITY
57+
// HIP hi:lo — the dual-use NEED pair
58+
FieldRef::new("aiwar:need/militaryUse", "militaryUse"), // 2 NEED
59+
FieldRef::new("aiwar:need/civicUse", "civicUse"), // 3 NEED
60+
// TWIG hi:lo
61+
FieldRef::new("aiwar:need/mlTask", "MLTask"), // 4 NEED (the task)
62+
FieldRef::new("aiwar:offer/mlType", "MLType"), // 5 OFFER (the technique)
63+
// LEAF hi:lo
64+
FieldRef::new("aiwar:intent/purpose", "purpose:vair"), // 6 INTENT (explicit)
65+
FieldRef::new("aiwar:offer/capacity", "capacity:airo"), // 7 OFFER
66+
// family hi:lo
4667
FieldRef::new("aiwar:offer/output", "output:airo"), // 8 OFFER
4768
FieldRef::new("aiwar:causality/impact", "impact:vair"), // 9 CAUSALITY (implicit)
69+
// identity hi:lo
4870
FieldRef::new("aiwar:relation/stakeholder", "stakeholder"), // 10 RELATION (edge)
49-
FieldRef::new("aiwar:person/motive", "motive"), // 11 PERSON (McClelland nPow/nAch/nAff)
71+
FieldRef::new("aiwar:person/airoRole", "airo:type"), // 11 PERSON (actor role)
5072
]
5173
});
5274

53-
/// The owner-authored ClassView for classid `0x0700`. Only `0x0700` resolves to
54-
/// the card; every other classid is the zero-fallback empty shape.
75+
/// The person card: 5 McClelland / Rubicon fields in GUID2 tier order. Every
76+
/// field is the `person` role — this is the Person side of Person×Situation
77+
/// (the trait), where the system card carries the Situation (need/offer/impact).
78+
static OSINT_PERSON_FIELDS: LazyLock<[FieldRef; 5]> = LazyLock::new(|| {
79+
[
80+
// HEEL hi:lo
81+
FieldRef::new("aiwar:person/stage", "stage"), // 0 Rubicon stage I..IV
82+
FieldRef::new("aiwar:person/need", "need"), // 1 McClelland nPow/nAch/nAff
83+
// HIP hi:lo
84+
FieldRef::new("aiwar:person/receptor", "receptor"), // 2 power receptor
85+
FieldRef::new("aiwar:person/rubicon", "rubicon"), // 3 Rubicon crossing
86+
// TWIG hi
87+
FieldRef::new("aiwar:person/motive", "motive"), // 4 dominant motive
88+
]
89+
});
90+
91+
/// The owner-authored ClassView for the OSINT domain. `0x0700` resolves to the
92+
/// AI-system card, `0x0701` to the person card; every other classid is the
93+
/// zero-fallback empty shape.
5594
pub struct OsintClassView;
5695

5796
impl ClassView for OsintClassView {
5897
fn fields(&self, class: ClassId) -> &[FieldRef] {
59-
if class == OSINT_CLASS {
60-
&OSINT_FIELDS[..]
61-
} else {
62-
&[]
98+
match class {
99+
OSINT_SYSTEM_CLASS => &OSINT_SYSTEM_FIELDS[..],
100+
OSINT_PERSON_CLASS => &OSINT_PERSON_FIELDS[..],
101+
_ => &[],
63102
}
64103
}
65104

@@ -73,96 +112,192 @@ impl ClassView for OsintClassView {
73112
}
74113
}
75114

76-
/// `?mask=<u64>` — the ViewFilter bitmask (bit i = show field i). Omitted = FULL.
115+
/// The human-readable concept name for a known OSINT classid (for the card
116+
/// header). Falls back to the hex id for anything else.
117+
fn concept_name(class: ClassId) -> &'static str {
118+
match class {
119+
OSINT_SYSTEM_CLASS => "osint_system",
120+
OSINT_PERSON_CLASS => "osint_person",
121+
_ => "unknown",
122+
}
123+
}
124+
125+
/// `?class=<u16>&mask=<u64>` — the ViewFilter. `class` omitted = the AI-system
126+
/// card (`0x0700`); `mask` omitted = FULL.
77127
#[derive(Deserialize)]
78128
pub struct CardQuery {
129+
class: Option<u16>,
79130
mask: Option<u64>,
80131
}
81132

82-
/// `GET /api/osint/card?mask=<bits>` — project the `0x0700` card through the
133+
impl CardQuery {
134+
fn resolve(&self) -> (ClassId, FieldMask) {
135+
let class = self.class.unwrap_or(OSINT_SYSTEM_CLASS);
136+
let mask = self.mask.map(FieldMask).unwrap_or(FieldMask::FULL);
137+
(class, mask)
138+
}
139+
}
140+
141+
/// `GET /api/osint/card?class=<id>&mask=<bits>` — project the card through the
83142
/// FieldMask and return the surviving `(label, predicate)` rows. This is the
84143
/// Redmine ERB ViewFilter, server-side: the mask selects the columns, the
85144
/// ClassView resolves the labels, nothing is computed on the client.
86145
pub async fn osint_card_handler(Query(q): Query<CardQuery>) -> Json<serde_json::Value> {
87-
let mask = q.mask.map(FieldMask).unwrap_or(FieldMask::FULL);
146+
let (class, mask) = q.resolve();
88147
let cv = OsintClassView;
89148
let rows: Vec<serde_json::Value> = cv
90-
.render_rows(OSINT_CLASS, mask)
149+
.render_rows(class, mask)
91150
.into_iter()
92151
.map(|r| serde_json::json!({ "label": r.label, "predicate": r.predicate }))
93152
.collect();
94153
Json(serde_json::json!({
95-
"classid": format!("0x{OSINT_CLASS:04x}"),
154+
"classid": format!("0x{class:04x}"),
155+
"concept": concept_name(class),
96156
"mask": mask.0,
97-
"field_count": cv.field_count(OSINT_CLASS),
157+
"field_count": cv.field_count(class),
98158
"shown": rows.len(),
99159
"rows": rows,
100160
}))
101161
}
102162

103-
/// `GET /api/osint/card.html?mask=<bits>` — the same ViewFilter, rendered
104-
/// server-side as HTML (the Redmine ERB view, in this codebase's `Html<String>`
105-
/// idiom). `render_rows` is template-agnostic, so this is a drop-in swap for an
106-
/// askama template once a version is pinned; the *projection* (mask → rows) is
107-
/// identical either way. Each row shows its reasoning role (parsed from the
108-
/// predicate prefix), the field label, and the predicate key.
163+
/// One projected card row — the askama template iterates these. `role` is the
164+
/// reasoning role parsed from the predicate prefix (`aiwar:<role>/<field>`).
165+
struct OsintCardRow {
166+
role: String,
167+
label: String,
168+
predicate: String,
169+
}
170+
171+
/// The card view — a dumb askama loop over the mask-filtered rows (the "XSLT"
172+
/// over the FieldMask projection). No per-field conditionals: the ViewFilter
173+
/// already carved the row set in Rust.
174+
#[derive(Template)]
175+
#[template(path = "osint_card.html")]
176+
struct OsintCardTemplate {
177+
classid_hex: String,
178+
concept: &'static str,
179+
mask_hex: String,
180+
shown: usize,
181+
total: usize,
182+
rows: Vec<OsintCardRow>,
183+
}
184+
185+
/// `GET /api/osint/card.html?class=<id>&mask=<bits>` — the same ViewFilter,
186+
/// rendered server-side via the compile-time-checked askama template. The
187+
/// *projection* (mask → rows) is identical to the JSON handler; askama is only
188+
/// the XSLT. Each row shows its reasoning role (parsed from the predicate
189+
/// prefix), the field label, and the predicate key.
109190
pub async fn osint_card_html_handler(Query(q): Query<CardQuery>) -> Html<String> {
110-
let mask = q.mask.map(FieldMask).unwrap_or(FieldMask::FULL);
191+
let (class, mask) = q.resolve();
111192
let cv = OsintClassView;
112-
let rows = cv.render_rows(OSINT_CLASS, mask);
113-
let mut body = String::new();
114-
for r in &rows {
115-
// predicate = "aiwar:<role>/<field>" — the reasoning role is the prefix.
116-
let role = r
117-
.predicate
118-
.strip_prefix("aiwar:")
119-
.and_then(|s| s.split('/').next())
120-
.unwrap_or("");
121-
body.push_str(&format!(
122-
"<tr><td class=r>{role}</td><td class=l>{}</td><td class=p>{}</td></tr>",
123-
r.label, r.predicate
124-
));
193+
let rows: Vec<OsintCardRow> = cv
194+
.render_rows(class, mask)
195+
.into_iter()
196+
.map(|r| {
197+
// predicate = "aiwar:<role>/<field>" — the reasoning role is the prefix.
198+
let role = r
199+
.predicate
200+
.strip_prefix("aiwar:")
201+
.and_then(|s| s.split('/').next())
202+
.unwrap_or("")
203+
.to_string();
204+
OsintCardRow {
205+
role,
206+
label: r.label.to_string(),
207+
predicate: r.predicate.to_string(),
208+
}
209+
})
210+
.collect();
211+
let tpl = OsintCardTemplate {
212+
classid_hex: format!("0x{class:04x}"),
213+
concept: concept_name(class),
214+
mask_hex: format!("0x{:03x}", mask.0),
215+
shown: rows.len(),
216+
total: cv.field_count(class),
217+
rows,
218+
};
219+
// askama render is infallible for this static template; fall back to a
220+
// terse error body rather than panicking in a request handler.
221+
match tpl.render() {
222+
Ok(body) => Html(body),
223+
Err(e) => Html(format!("<pre>osint card render error: {e}</pre>")),
125224
}
126-
Html(format!(
127-
"<!doctype html><meta charset=utf-8><title>OSINT 0x0700 card</title>\
128-
<style>body{{background:#0a0e17;color:#cfe7ff;font:13px ui-monospace,monospace;padding:18px}}\
129-
h1{{font-size:14px;color:#7fd1ff;font-weight:400}}table{{border-collapse:collapse;margin-top:10px}}\
130-
td{{padding:4px 12px;border-bottom:1px solid #1b2c3e;white-space:nowrap}}\
131-
.r{{color:#7fd1ff}}.l{{color:#eaf4ff}}.p{{color:#5a6b7f}}</style>\
132-
<h1>OSINT · classid 0x0700 · FieldMask 0x{:03x} · {} / {} fields shown</h1>\
133-
<table><tr><td class=r>role</td><td class=l>field</td><td class=p>predicate</td></tr>{}</table>",
134-
mask.0,
135-
rows.len(),
136-
cv.field_count(OSINT_CLASS),
137-
body
138-
))
139225
}
140226

141227
#[cfg(test)]
142228
mod tests {
143229
use super::*;
144230

145231
#[test]
146-
fn card_has_twelve_fields_in_bit_order() {
232+
fn system_card_has_twelve_fields_in_tier_order() {
147233
let cv = OsintClassView;
148-
assert_eq!(cv.field_count(OSINT_CLASS), 12);
149-
assert_eq!(cv.field_label(OSINT_CLASS, 0), Some("militaryUse"));
150-
assert_eq!(cv.field_label(OSINT_CLASS, 9), Some("impact:vair"));
151-
assert_eq!(cv.field_label(OSINT_CLASS, 11), Some("motive"));
234+
assert_eq!(cv.field_count(OSINT_SYSTEM_CLASS), 12);
235+
// GUID1 tier order: HEEL currentStatus:type, HIP mil:civ, …
236+
assert_eq!(cv.field_label(OSINT_SYSTEM_CLASS, 0), Some("currentStatus"));
237+
assert_eq!(cv.field_label(OSINT_SYSTEM_CLASS, 2), Some("militaryUse"));
238+
assert_eq!(cv.field_label(OSINT_SYSTEM_CLASS, 9), Some("impact:vair"));
239+
assert_eq!(cv.field_label(OSINT_SYSTEM_CLASS, 11), Some("airo:type"));
152240
// unknown class = zero-fallback empty shape.
153241
assert_eq!(cv.field_count(0x0000), 0);
154242
}
155243

244+
#[test]
245+
fn person_card_has_five_mcclelland_fields() {
246+
let cv = OsintClassView;
247+
assert_eq!(cv.field_count(OSINT_PERSON_CLASS), 5);
248+
assert_eq!(cv.field_label(OSINT_PERSON_CLASS, 0), Some("stage"));
249+
assert_eq!(cv.field_label(OSINT_PERSON_CLASS, 1), Some("need"));
250+
assert_eq!(cv.field_label(OSINT_PERSON_CLASS, 4), Some("motive"));
251+
}
252+
156253
#[test]
157254
fn field_mask_is_the_view_filter() {
158255
let cv = OsintClassView;
159-
// full mask → all 12 rows.
160-
assert_eq!(cv.render_rows(OSINT_CLASS, FieldMask::FULL).len(), 12);
161-
// mask with only the Causality axis ends (intent bit 4 + impact bit 9).
162-
let causal = FieldMask::EMPTY.with(4).with(9);
163-
let rows = cv.render_rows(OSINT_CLASS, causal);
256+
// full mask → all 12 system rows.
257+
assert_eq!(
258+
cv.render_rows(OSINT_SYSTEM_CLASS, FieldMask::FULL).len(),
259+
12
260+
);
261+
// mask with only the Causality axis ends (intent bit 6 + impact bit 9).
262+
let causal = FieldMask::EMPTY.with(6).with(9);
263+
let rows = cv.render_rows(OSINT_SYSTEM_CLASS, causal);
164264
assert_eq!(rows.len(), 2);
165265
assert_eq!(rows[0].label, "purpose:vair");
166266
assert_eq!(rows[1].label, "impact:vair");
167267
}
268+
269+
#[test]
270+
fn html_card_renders_through_askama() {
271+
// Smoke the askama path end-to-end for the person card: header +
272+
// one row per selected field, role parsed from the predicate prefix.
273+
let cv = OsintClassView;
274+
let rows: Vec<OsintCardRow> = cv
275+
.render_rows(OSINT_PERSON_CLASS, FieldMask::FULL)
276+
.into_iter()
277+
.map(|r| OsintCardRow {
278+
role: r
279+
.predicate
280+
.strip_prefix("aiwar:")
281+
.and_then(|s| s.split('/').next())
282+
.unwrap_or("")
283+
.to_string(),
284+
label: r.label.to_string(),
285+
predicate: r.predicate.to_string(),
286+
})
287+
.collect();
288+
let tpl = OsintCardTemplate {
289+
classid_hex: "0x0701".to_string(),
290+
concept: "osint_person",
291+
mask_hex: "0x01f".to_string(),
292+
shown: rows.len(),
293+
total: 5,
294+
rows,
295+
};
296+
let body = tpl.render().expect("askama render");
297+
assert!(body.contains("osint_person"));
298+
assert!(body.contains("motive"));
299+
assert!(body.contains("aiwar:person/motive"));
300+
// dumb-loop template: every selected field is a row.
301+
assert_eq!(body.matches("<tr>").count(), 5 + 1); // 5 rows + header row
302+
}
168303
}

0 commit comments

Comments
 (0)