@@ -50,8 +50,9 @@ use crate::wire::{
5050 WireCalibrateRequest , WireCalibrateResponse , WireCrystal , WireDispatch , WireHealth ,
5151 WireIngest , WirePlanRequest , WirePlanResponse , WireProbeRequest , WireProbeResponse ,
5252 WireQualia , WireRunbookRequest , WireRunbookResponse , WireRunbookStep ,
53- WireRunbookStepResult , WireStepResult , WireStyleInfo , WireTensorsRequest ,
54- WireTensorsResponse , WireTokenAgreement , WireTokenAgreementResult , WireUnifiedStep ,
53+ WireRunbookStepResult , WireStepResult , WireStyleInfo , WireSweepRequest ,
54+ WireSweepResponse , WireSweepResult , WireTensorsRequest , WireTensorsResponse ,
55+ WireTokenAgreement , WireTokenAgreementResult , WireUnifiedStep ,
5556} ;
5657use lance_graph_contract:: cam:: CodecParams ;
5758use std:: path:: Path as StdPath ;
@@ -96,6 +97,13 @@ pub fn router(driver: ShaderDriver) -> Router {
9697 // `backend:"stub"` so clients cannot confuse Phase 0 stub output
9798 // for a real measurement (anti-#219 defense, type-level).
9899 . route ( "/v1/shader/token-agreement" , post ( token_agreement_handler) )
100+ // D3.1 — codec sweep endpoint (batch mode). Client POSTs a
101+ // WireSweepRequest containing a cross-product grid; handler
102+ // enumerates grid, validates each candidate, builds stub results,
103+ // returns WireSweepResponse. SSE streaming + Lance append land in
104+ // D3.1b; this batch path stays for clients that want all results
105+ // in one response without streaming.
106+ . route ( "/v1/shader/sweep" , post ( sweep_handler) )
99107 // Scheduled runbook: one POST runs a list of steps. Test injection
100108 // lands here — a client script submits its full codec-research
101109 // protocol as a single DTO, the server executes and returns all
@@ -284,6 +292,76 @@ async fn token_agreement_handler(
284292 . map_err ( |e| ( StatusCode :: BAD_REQUEST , Json ( json ! ( { "error" : format!( "{e}" ) } ) ) ) )
285293}
286294
295+ /// D3.1 — `POST /v1/shader/sweep` handler (batch mode).
296+ ///
297+ /// Enumerates the cross-product grid from `WireSweepRequest`, validates
298+ /// each candidate via TryFrom(CodecParams), computes kernel_signature +
299+ /// backend per point, and returns all results in one `WireSweepResponse`.
300+ ///
301+ /// Stub: per-point calibrate/token_agreement are `None`; Phase 3 real
302+ /// handler invokes the actual codec_research + token_agreement harness.
303+ /// SSE streaming variant (D3.1b) replaces the batch return with per-point
304+ /// Server-Sent Events.
305+ async fn sweep_handler (
306+ Json ( req) : Json < WireSweepRequest > ,
307+ ) -> Result < Json < WireSweepResponse > , ( StatusCode , Json < Value > ) > {
308+ let start = std:: time:: Instant :: now ( ) ;
309+
310+ // P1 — reject oversized grids before materialization. A small JSON
311+ // payload with moderately-sized axes can explode into a huge Cartesian
312+ // product; bound it so the endpoint isn't a DoS vector.
313+ const MAX_GRID_CARDINALITY : usize = 10_000 ;
314+ let cardinality = req. grid . cardinality ( ) ;
315+ if cardinality > MAX_GRID_CARDINALITY {
316+ return Err ( (
317+ StatusCode :: BAD_REQUEST ,
318+ Json ( json ! ( {
319+ "error" : format!(
320+ "sweep grid cardinality {cardinality} exceeds max {MAX_GRID_CARDINALITY}; \
321+ reduce axis dimensions"
322+ )
323+ } ) ) ,
324+ ) ) ;
325+ }
326+
327+ let candidates = req. grid . enumerate ( ) ;
328+
329+ let mut results = Vec :: with_capacity ( candidates. len ( ) ) ;
330+ for ( idx, wire_params) in candidates. into_iter ( ) . enumerate ( ) {
331+ // Validate each grid point at ingress — surface typed errors early.
332+ let params: CodecParams = wire_params
333+ . clone ( )
334+ . try_into ( )
335+ . map_err ( |e : lance_graph_contract:: cam:: CodecParamsError | {
336+ ( StatusCode :: BAD_REQUEST , Json ( json ! ( {
337+ "error" : format!( "grid point {idx}: invalid CodecParams: {e}" )
338+ } ) ) )
339+ } ) ?;
340+
341+ results. push ( WireSweepResult {
342+ grid_index : idx as u32 ,
343+ candidate : wire_params,
344+ kernel_hash : params. kernel_signature ( ) ,
345+ calibrate : None ,
346+ token_agreement : None ,
347+ stub : true ,
348+ } ) ;
349+ }
350+
351+ Ok ( Json ( WireSweepResponse {
352+ label : req. label ,
353+ cardinality : cardinality as u32 ,
354+ results,
355+ elapsed_ms : start. elapsed ( ) . as_millis ( ) as u64 ,
356+ // P2 — do NOT echo req.log_to_lance into the response when no rows
357+ // were actually written. Clients that treat lance_fragment_path as
358+ // evidence of successful logging would silently skip retries and
359+ // lose experiment results. Set to None until the real Lance append
360+ // writer lands (Phase 3 D3.1b).
361+ lance_fragment_path : None ,
362+ } ) )
363+ }
364+
287365async fn route_handler (
288366 State ( _state) : State < AppState > ,
289367 Json ( wire) : Json < WireUnifiedStep > ,
0 commit comments