@@ -394,6 +394,50 @@ pub struct OctadResponse {
394394 pub has_spatial : bool ,
395395 pub version_count : u64 ,
396396 pub provenance_chain_length : u64 ,
397+ /// Populated only when the caller passes `?include=types` on a GET. Listed
398+ /// in the order the entity declares them. Cheap to surface but opt-in so
399+ /// the default response shape stays small.
400+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
401+ pub semantic_types : Option < Vec < String > > ,
402+ }
403+
404+ /// Per-request flags controlling which expensive or large fields the GET
405+ /// /octads/{id} response should include. Driven by `?include=…` (a
406+ /// comma-separated list).
407+ #[ derive( Debug , Default , Clone , Copy ) ]
408+ pub struct IncludeFlags {
409+ /// Surface `semantic_types` (the entity's declared type IRIs)
410+ pub types : bool ,
411+ /// Surface the stored vector bytes (Phase 1 Step 3 — Task #12)
412+ pub embedding : bool ,
413+ }
414+
415+ impl IncludeFlags {
416+ /// Parse a `?include=` value (comma-separated). Unknown tokens are ignored
417+ /// rather than errored — the API's forward-compatibility contract is that
418+ /// a future client may request includes the current server doesn't know
419+ /// about.
420+ pub fn parse ( raw : Option < & str > ) -> Self {
421+ let mut flags = Self :: default ( ) ;
422+ if let Some ( s) = raw {
423+ for token in s. split ( ',' ) . map ( |t| t. trim ( ) ) . filter ( |t| !t. is_empty ( ) ) {
424+ match token {
425+ "types" => flags. types = true ,
426+ "embedding" => flags. embedding = true ,
427+ _ => { }
428+ }
429+ }
430+ }
431+ flags
432+ }
433+ }
434+
435+ /// Query parameters for `GET /octads/{id}`
436+ #[ derive( Debug , Deserialize ) ]
437+ pub struct OctadGetQuery {
438+ /// Comma-separated list of optional includes — currently `types`,
439+ /// `embedding`. Unknown tokens are ignored.
440+ pub include : Option < String > ,
397441}
398442
399443/// Status response
@@ -411,6 +455,24 @@ pub struct OctadStatusResponse {
411455
412456impl From < & verisim_octad:: Octad > for OctadResponse {
413457 fn from ( h : & verisim_octad:: Octad ) -> Self {
458+ Self :: with_includes ( h, IncludeFlags :: default ( ) )
459+ }
460+ }
461+
462+ impl OctadResponse {
463+ /// Build a response from an octad, honouring per-request include flags
464+ /// for opt-in fields (semantic types, vector bytes, …).
465+ pub fn with_includes ( h : & verisim_octad:: Octad , flags : IncludeFlags ) -> Self {
466+ let semantic_types = if flags. types {
467+ Some (
468+ h. semantic
469+ . as_ref ( )
470+ . map ( |s| s. types . clone ( ) )
471+ . unwrap_or_default ( ) ,
472+ )
473+ } else {
474+ None
475+ } ;
414476 Self {
415477 id : h. id . to_string ( ) ,
416478 status : OctadStatusResponse {
@@ -428,6 +490,7 @@ impl From<&verisim_octad::Octad> for OctadResponse {
428490 has_spatial : h. spatial_data . is_some ( ) ,
429491 version_count : h. version_count ,
430492 provenance_chain_length : h. provenance_chain_length ,
493+ semantic_types,
431494 }
432495 }
433496}
@@ -901,6 +964,7 @@ async fn create_octad_handler(
901964async fn get_octad_handler (
902965 State ( state) : State < AppState > ,
903966 Path ( id) : Path < String > ,
967+ Query ( query) : Query < OctadGetQuery > ,
904968) -> Result < Json < OctadResponse > , ApiError > {
905969 validate_octad_id ( & id) ?;
906970 let octad_id = OctadId :: new ( & id) ;
@@ -912,7 +976,8 @@ async fn get_octad_handler(
912976 . map_err ( |e| ApiError :: Internal ( e. to_string ( ) ) ) ?
913977 . ok_or_else ( || ApiError :: NotFound ( format ! ( "Octad {} not found" , id) ) ) ?;
914978
915- Ok ( Json ( OctadResponse :: from ( & octad) ) )
979+ let flags = IncludeFlags :: parse ( query. include . as_deref ( ) ) ;
980+ Ok ( Json ( OctadResponse :: with_includes ( & octad, flags) ) )
916981}
917982
918983/// Update octad handler
@@ -2316,6 +2381,107 @@ mod tests {
23162381 assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
23172382 }
23182383
2384+ #[ tokio:: test]
2385+ async fn test_include_types_round_trip ( ) {
2386+ // Phase 1 gap closure: GET /octads/{id}?include=types must surface the
2387+ // entity's declared semantic types as semantic_types: Vec<String>. The
2388+ // default GET (no include) must still omit the field so legacy clients
2389+ // see no schema change.
2390+ let state = create_test_state ( ) . await ;
2391+ let app = build_router ( state) ;
2392+
2393+ let create_request = OctadRequest {
2394+ title : Some ( "Typed entity" . to_string ( ) ) ,
2395+ body : Some ( "Body" . to_string ( ) ) ,
2396+ embedding : Some ( vec ! [ 0.1 , 0.2 , 0.3 ] ) ,
2397+ types : Some ( vec ! [
2398+ "https://example.org/Email" . to_string( ) ,
2399+ "https://schema.org/Message" . to_string( ) ,
2400+ ] ) ,
2401+ relationships : None ,
2402+ tensor : None ,
2403+ temporal : None ,
2404+ metadata : None ,
2405+ provenance : None ,
2406+ spatial : None ,
2407+ } ;
2408+ let response = app
2409+ . clone ( )
2410+ . oneshot (
2411+ Request :: builder ( )
2412+ . method ( "POST" )
2413+ . uri ( "/octads" )
2414+ . header ( "content-type" , "application/json" )
2415+ . body ( Body :: from (
2416+ serde_json:: to_string ( & create_request) . expect ( "serialize create request" ) ,
2417+ ) )
2418+ . expect ( "build POST request" ) ,
2419+ )
2420+ . await
2421+ . expect ( "oneshot create" ) ;
2422+ assert_eq ! ( response. status( ) , StatusCode :: CREATED ) ;
2423+
2424+ let body = axum:: body:: to_bytes ( response. into_body ( ) , 1024 * 1024 )
2425+ . await
2426+ . expect ( "read create body" ) ;
2427+ let created: OctadResponse =
2428+ serde_json:: from_slice ( & body) . expect ( "decode create response" ) ;
2429+ assert ! (
2430+ created. semantic_types. is_none( ) ,
2431+ "default response (POST) must not surface semantic_types"
2432+ ) ;
2433+
2434+ // GET without include — types still omitted
2435+ let response = app
2436+ . clone ( )
2437+ . oneshot (
2438+ Request :: builder ( )
2439+ . uri ( format ! ( "/octads/{}" , created. id) )
2440+ . body ( Body :: empty ( ) )
2441+ . expect ( "build GET request" ) ,
2442+ )
2443+ . await
2444+ . expect ( "oneshot get" ) ;
2445+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
2446+ let body = axum:: body:: to_bytes ( response. into_body ( ) , 1024 * 1024 )
2447+ . await
2448+ . expect ( "read get body" ) ;
2449+ let got: OctadResponse = serde_json:: from_slice ( & body) . expect ( "decode get response" ) ;
2450+ assert ! (
2451+ got. semantic_types. is_none( ) ,
2452+ "default GET must not surface semantic_types"
2453+ ) ;
2454+
2455+ // GET with ?include=types — types surfaced verbatim
2456+ let response = app
2457+ . oneshot (
2458+ Request :: builder ( )
2459+ . uri ( format ! ( "/octads/{}?include=types" , created. id) )
2460+ . body ( Body :: empty ( ) )
2461+ . expect ( "build GET request" ) ,
2462+ )
2463+ . await
2464+ . expect ( "oneshot get with include" ) ;
2465+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
2466+ let body = axum:: body:: to_bytes ( response. into_body ( ) , 1024 * 1024 )
2467+ . await
2468+ . expect ( "read get body" ) ;
2469+ let got: OctadResponse =
2470+ serde_json:: from_slice ( & body) . expect ( "decode get response with include" ) ;
2471+ let types = got
2472+ . semantic_types
2473+ . as_ref ( )
2474+ . expect ( "?include=types must surface semantic_types" ) ;
2475+ assert_eq ! (
2476+ types,
2477+ & vec![
2478+ "https://example.org/Email" . to_string( ) ,
2479+ "https://schema.org/Message" . to_string( ) ,
2480+ ] ,
2481+ "semantic_types must round-trip in declaration order"
2482+ ) ;
2483+ }
2484+
23192485 #[ tokio:: test]
23202486 async fn test_temporal_observed_at_round_trip ( ) {
23212487 // Phase 1 gap closure: caller-supplied real-world time (e.g. an email's
0 commit comments