@@ -3,6 +3,10 @@ import { toError } from '@sim/utils/errors'
33import { omit } from '@sim/utils/object'
44import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
55import { materializeLargeValueRef , storeLargeValue } from '@/lib/execution/payloads/store'
6+ import {
7+ collectFunctionalBlockOutputs ,
8+ type FunctionalExecutionDataSource ,
9+ } from '@/lib/logs/execution/functional-outputs'
610import { projectTraceSpansForSecrets } from '@/lib/logs/execution/trace-secret-projection'
711import type { TraceSpan } from '@/lib/logs/types'
812import {
@@ -72,6 +76,11 @@ export interface TraceStoreReadContext {
7276 userId ?: string
7377}
7478
79+ export interface DisplayExecutionDataWithBlockOutputs {
80+ executionData : Record < string , unknown >
81+ blockOutputs : Map < string , unknown >
82+ }
83+
7584/**
7685 * Write-path context. Requires the execution owner's `userId`: the externalized
7786 * object is tracked in `workspace_files`, whose `user_id` column is NOT NULL
@@ -269,6 +278,102 @@ export async function materializeExecutionDataForDisplay(
269278 return projectExecutionDataForDisplay ( materialized , context )
270279}
271280
281+ /**
282+ * Materializes one trusted row into its display envelope plus secret-safe functional outputs.
283+ * Execution-state output remains authoritative when present, but only requested blocks are
284+ * projected and returned; the raw execution state never crosses the display boundary.
285+ */
286+ export async function materializeExecutionDataForDisplayWithBlockOutputs (
287+ executionData : Record < string , unknown > | null | undefined ,
288+ context : TraceStoreReadContext ,
289+ blockIds : readonly string [ ]
290+ ) : Promise < DisplayExecutionDataWithBlockOutputs > {
291+ const materialized = await materializeExecutionData ( executionData , context )
292+ const displayData = await projectExecutionDataForDisplay ( materialized , context )
293+ if ( blockIds . length === 0 ) {
294+ return { executionData : displayData , blockOutputs : new Map ( ) }
295+ }
296+
297+ const executionState = readRecord ( materialized . executionState )
298+ const blockStates = readRecord ( executionState ?. blockStates )
299+ if ( ! blockStates ) {
300+ return {
301+ executionData : displayData ,
302+ blockOutputs : collectFunctionalBlockOutputs (
303+ displayData as FunctionalExecutionDataSource | undefined
304+ ) ,
305+ }
306+ }
307+
308+ const runRegistry = await importResolvedSecretTraceRegistry (
309+ materialized [ RESOLVED_SECRET_PROVENANCE_KEY ] ??
310+ executionState ?. [ RESOLVED_SECRET_PROVENANCE_KEY ] ,
311+ 'traceStore.blockOutputRunProvenance'
312+ )
313+ const blockOutputs = new Map < string , unknown > ( )
314+ const projectionStore = createReadOnlyProjectionStore ( context )
315+
316+ for ( const blockId of new Set ( blockIds ) ) {
317+ const blockState = readRecord ( blockStates [ blockId ] )
318+ if ( ! blockState || blockState . output === undefined ) continue
319+
320+ const hasExactProvenance = Object . hasOwn ( blockState , RESOLVED_SECRET_PROVENANCE_KEY )
321+ const registry = hasExactProvenance
322+ ? await importResolvedSecretTraceRegistry (
323+ blockState [ RESOLVED_SECRET_PROVENANCE_KEY ] ,
324+ 'traceStore.blockOutputExactProvenance'
325+ )
326+ : runRegistry
327+ const now = new Date ( ) . toISOString ( )
328+ const [ projected ] = await projectTraceSpansForSecrets (
329+ [
330+ {
331+ id : `${ LOG_DISPLAY_PROJECTION_SPAN_ID } -block-output` ,
332+ name : 'Block Output Display Projection' ,
333+ type : 'display' ,
334+ duration : 0 ,
335+ startTime : now ,
336+ endTime : now ,
337+ output : { value : blockState . output } ,
338+ } ,
339+ ] ,
340+ { registry, allowLargeValueWrites : false , store : projectionStore }
341+ )
342+ if ( projected ?. output && Object . hasOwn ( projected . output , 'value' ) ) {
343+ blockOutputs . set ( blockId , projected . output . value )
344+ }
345+ }
346+
347+ return { executionData : displayData , blockOutputs }
348+ }
349+
350+ function readRecord ( value : unknown ) : Record < string , unknown > | undefined {
351+ return value && typeof value === 'object' && ! Array . isArray ( value )
352+ ? ( value as Record < string , unknown > )
353+ : undefined
354+ }
355+
356+ async function importResolvedSecretTraceRegistry (
357+ provenance : unknown ,
358+ origin : string
359+ ) : Promise < ResolvedSecretTraceRegistry | undefined > {
360+ if ( ! isResolvedSecretTraceProvenanceV1 ( provenance ) ) return undefined
361+
362+ const registry = new ResolvedSecretTraceRegistry ( [ ] , provenance . scope )
363+ await registry . importProvenance ( provenance , { trusted : true , origin } )
364+ return registry
365+ }
366+
367+ function createReadOnlyProjectionStore ( context : TraceStoreReadContext ) {
368+ return {
369+ workspaceId : context . workspaceId ?? undefined ,
370+ workflowId : context . workflowId ?? undefined ,
371+ executionId : context . executionId ,
372+ userId : context . userId ,
373+ trackReference : false ,
374+ }
375+ }
376+
272377/**
273378 * Projects execution-log content with the encrypted provenance saved by the
274379 * trusted executor. Current workflow input and final output values use their
@@ -284,12 +389,7 @@ export async function projectExecutionDataForDisplay(
284389 executionData : Record < string , unknown > ,
285390 context : TraceStoreReadContext
286391) : Promise < Record < string , unknown > > {
287- const executionState =
288- executionData . executionState &&
289- typeof executionData . executionState === 'object' &&
290- ! Array . isArray ( executionData . executionState )
291- ? ( executionData . executionState as Record < string , unknown > )
292- : undefined
392+ const executionState = readRecord ( executionData . executionState )
293393 const hasTopLevelProvenance = Object . hasOwn ( executionData , RESOLVED_SECRET_PROVENANCE_KEY )
294394 const stateProvenance = executionState ?. [ RESOLVED_SECRET_PROVENANCE_KEY ]
295395 const provenance = executionData [ RESOLVED_SECRET_PROVENANCE_KEY ] ?? stateProvenance
@@ -302,15 +402,7 @@ export async function projectExecutionDataForDisplay(
302402 return projectLegacyExecutionDataForDisplay ( executionData )
303403 }
304404
305- let registry : ResolvedSecretTraceRegistry | undefined
306-
307- if ( isResolvedSecretTraceProvenanceV1 ( provenance ) ) {
308- registry = new ResolvedSecretTraceRegistry ( [ ] , provenance . scope )
309- await registry . importProvenance ( provenance , {
310- trusted : true ,
311- origin : 'traceStore.spanProvenance' ,
312- } )
313- }
405+ const registry = await importResolvedSecretTraceRegistry ( provenance , 'traceStore.spanProvenance' )
314406
315407 /**
316408 * Compaction drops `executionState`, and with it the only copy of the
@@ -339,13 +431,7 @@ export async function projectExecutionDataForDisplay(
339431 } )
340432 }
341433
342- const projectionStore = {
343- workspaceId : context . workspaceId ?? undefined ,
344- workflowId : context . workflowId ?? undefined ,
345- executionId : context . executionId ,
346- userId : context . userId ,
347- trackReference : false ,
348- }
434+ const projectionStore = createReadOnlyProjectionStore ( context )
349435
350436 const exactValueProjections = new Map < string , unknown > ( )
351437 for ( const [ valueKey , provenanceKey ] of Object . entries ( EXACT_LOG_VALUE_PROVENANCE_KEYS ) ) {
0 commit comments