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
2134use std:: sync:: LazyLock ;
2235
36+ use askama:: Template ;
2337use axum:: response:: Html ;
2438use axum:: { extract:: Query , Json } ;
2539use lance_graph_contract:: class_view:: { ClassId , ClassView , FieldMask } ;
2640use lance_graph_contract:: ontology:: { DisplayTemplate , FieldRef } ;
2741use 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.
5594pub struct OsintClassView ;
5695
5796impl 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 ) ]
78128pub 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.
86145pub 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.
109190pub 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) ]
142228mod 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