@@ -300,36 +300,29 @@ export async function materializeExecutionDataForDisplayWithBlockOutputs(
300300 return { executionData : displayData , blockOutputs : new Map ( ) }
301301 }
302302
303- const runProvenance =
304- materialized [ RESOLVED_SECRET_PROVENANCE_KEY ] ?? executionState ?. [ RESOLVED_SECRET_PROVENANCE_KEY ]
305- const runRegistry = await importResolvedSecretTraceRegistry (
306- runProvenance ,
303+ const runImport = await importStoredDisplayEnvelope (
304+ materialized [ RESOLVED_SECRET_PROVENANCE_KEY ] ??
305+ executionState ?. [ RESOLVED_SECRET_PROVENANCE_KEY ] ,
307306 'traceStore.blockOutputRunProvenance'
308307 )
309308 const provenanceFaults = new Map < string , StoredDisplayProvenanceFault > ( )
310- if ( isIncompleteStoredEnvelope ( runProvenance ) ) {
311- provenanceFaults . set ( 'run' , 'incomplete' )
312- }
309+ if ( runImport . fault ) provenanceFaults . set ( 'run' , runImport . fault )
313310 const blockOutputs = new Map < string , unknown > ( )
314311 const projectionStore = createReadOnlyProjectionStore ( context )
315312
316313 for ( const blockId of new Set ( blockIds ) ) {
317314 const blockState = readRecord ( blockStates [ blockId ] )
318315 if ( ! blockState || blockState . output === undefined ) continue
319316
320- const hasExactProvenance = Object . hasOwn ( blockState , RESOLVED_SECRET_PROVENANCE_KEY )
321- if (
322- hasExactProvenance &&
323- isIncompleteStoredEnvelope ( blockState [ RESOLVED_SECRET_PROVENANCE_KEY ] )
324- ) {
325- provenanceFaults . set ( `blockOutput:${ blockId } ` , 'incomplete' )
317+ let registry = runImport . registry
318+ if ( Object . hasOwn ( blockState , RESOLVED_SECRET_PROVENANCE_KEY ) ) {
319+ const blockImport = await importStoredDisplayEnvelope (
320+ blockState [ RESOLVED_SECRET_PROVENANCE_KEY ] ,
321+ 'traceStore.blockOutputExactProvenance'
322+ )
323+ if ( blockImport . fault ) provenanceFaults . set ( `blockOutput:${ blockId } ` , blockImport . fault )
324+ registry = blockImport . registry
326325 }
327- const registry = hasExactProvenance
328- ? await importResolvedSecretTraceRegistry (
329- blockState [ RESOLVED_SECRET_PROVENANCE_KEY ] ,
330- 'traceStore.blockOutputExactProvenance'
331- )
332- : runRegistry
333326 const now = new Date ( ) . toISOString ( )
334327 const [ projected ] = await projectTraceSpansForSecrets (
335328 [
@@ -358,73 +351,96 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
358351 return isRecordLike ( value ) ? ( value as Record < string , unknown > ) : undefined
359352}
360353
354+ type StoredDisplayProvenanceFault = 'incomplete' | 'malformed' | 'undecryptable'
355+
356+ interface StoredDisplayEnvelopeImport {
357+ registry : ResolvedSecretTraceRegistry | undefined
358+ fault : StoredDisplayProvenanceFault | undefined
359+ }
360+
361361/**
362362 * Staged: display registries filter stored values for one materialization and are discarded, and
363363 * their own mark-time summaries name no execution — the read boundary reports instead, through
364364 * {@link reportStoredDisplayProvenanceFaults}. A stored envelope's incompleteness is not an event
365365 * on this path; it was recorded when the run wrote it, and every later view re-derives it.
366+ *
367+ * The fault is classified where the import happens so every consumer reports the same way: an
368+ * absent envelope is not a fault (truncation has its own warning), a present value that does not
369+ * parse is `malformed`, a parsed envelope that cannot vouch is `incomplete`, and a complete
370+ * envelope whose registry latched during import — entry decryption is the only latch on this
371+ * trusted path — is `undecryptable`. Projection withholds the guarded values in all three cases.
366372 */
367- async function importResolvedSecretTraceRegistry (
373+ async function importStoredDisplayEnvelope (
368374 provenance : unknown ,
369375 origin : string
370- ) : Promise < ResolvedSecretTraceRegistry | undefined > {
371- if ( ! isResolvedSecretTraceProvenanceV1 ( provenance ) ) return undefined
376+ ) : Promise < StoredDisplayEnvelopeImport > {
377+ if ( provenance === undefined ) return { registry : undefined , fault : undefined }
378+ if ( ! isResolvedSecretTraceProvenanceV1 ( provenance ) ) {
379+ return { registry : undefined , fault : 'malformed' }
380+ }
372381
373382 const registry = new ResolvedSecretTraceRegistry ( [ ] , provenance . scope , { staged : true } )
374383 await registry . importProvenance ( provenance , { trusted : true , origin } )
375- return registry
376- }
377-
378- type StoredDisplayProvenanceFault = 'incomplete' | 'malformed'
379-
380- /** True for a stored envelope that parses but cannot vouch for the value it accompanies. */
381- function isIncompleteStoredEnvelope ( value : unknown ) : boolean {
382- return isResolvedSecretTraceProvenanceV1 ( value ) && ! value . complete
384+ const fault = ! provenance . complete
385+ ? 'incomplete'
386+ : registry . isPermanentlyIncomplete ( )
387+ ? 'undecryptable'
388+ : undefined
389+ return { registry, fault }
383390}
384391
385392const MAX_REPORTED_PROVENANCE_FAULT_PARTS = 20
386393
394+ const STORED_PROVENANCE_FAULT_REPORTS = {
395+ incomplete : {
396+ level : 'warn' ,
397+ message : 'Stored execution provenance cannot vouch for display content' ,
398+ } ,
399+ malformed : { level : 'error' , message : 'Stored execution provenance is malformed' } ,
400+ /** The entry-level decrypt error already logs its counts; this adds the execution it hit. */
401+ undecryptable : { level : 'error' , message : 'Stored execution provenance could not be decrypted' } ,
402+ } as const satisfies Record <
403+ StoredDisplayProvenanceFault ,
404+ { level : 'warn' | 'error' ; message : string }
405+ >
406+
387407/**
388- * One attributed line per display function per materialization , in place of one registry summary
389- * per envelope per view.
408+ * One attributed line per fault kind per display function , in place of one registry summary per
409+ * envelope per view.
390410 *
391411 * The registry summaries these replace carried counts and a workspace but no execution id, so a
392412 * reader repeatedly materializing the same stored rows produced an unattributable stream — the
393- * lines could not say which executions to go look at. Incomplete stays at warn (a stored state
394- * being re-read); malformed stays at error (a stored envelope that cannot be parsed is a fault
395- * wherever it is met, matching the level its registry reason carries elsewhere ).
413+ * lines could not say which executions to go look at. Severity follows the registry reason each
414+ * fault replaces: incomplete at warn (a stored state being re-read), malformed and undecryptable
415+ * at error (faults wherever they are met ).
396416 *
397- * A block-outputs read runs the display projection first, so an incomplete run envelope appears
398- * once under each site — `traceSpans` guarding the span projection, `run` as the block fallback.
399- * Two sites reading the same envelope are two facts about the view; collapsing them would couple
400- * the display functions to share reporting state for one line less.
417+ * A block-outputs read runs the display projection first, so a faulted run envelope appears once
418+ * under each site — `traceSpans` guarding the span projection, `run` as the block fallback. Two
419+ * sites reading the same envelope are two facts about the view; collapsing them would couple the
420+ * display functions to share reporting state for one line less.
401421 */
402422function reportStoredDisplayProvenanceFaults (
403423 site : string ,
404424 context : TraceStoreReadContext ,
405425 faults : ReadonlyMap < string , StoredDisplayProvenanceFault >
406426) : void {
407427 if ( faults . size === 0 ) return
408- const partsByFault = { incomplete : [ ] as string [ ] , malformed : [ ] as string [ ] }
409- for ( const [ part , fault ] of faults ) partsByFault [ fault ] . push ( part )
410428 const details = {
411429 site,
412430 executionId : context . executionId ,
413431 ...( context . workflowId ? { workflowId : context . workflowId } : { } ) ,
414432 ...( context . workspaceId ? { workspaceId : context . workspaceId } : { } ) ,
415433 }
416- if ( partsByFault . incomplete . length > 0 ) {
417- logger . warn ( 'Stored execution provenance cannot vouch for display content' , {
434+ for ( const [ kind , report ] of Object . entries ( STORED_PROVENANCE_FAULT_REPORTS ) as [
435+ StoredDisplayProvenanceFault ,
436+ ( typeof STORED_PROVENANCE_FAULT_REPORTS ) [ StoredDisplayProvenanceFault ] ,
437+ ] [ ] ) {
438+ const parts = [ ...faults ] . filter ( ( [ , fault ] ) => fault === kind ) . map ( ( [ part ] ) => part )
439+ if ( parts . length === 0 ) continue
440+ logger [ report . level ] ( report . message , {
418441 ...details ,
419- parts : partsByFault . incomplete . slice ( 0 , MAX_REPORTED_PROVENANCE_FAULT_PARTS ) ,
420- partCount : partsByFault . incomplete . length ,
421- } )
422- }
423- if ( partsByFault . malformed . length > 0 ) {
424- logger . error ( 'Stored execution provenance is malformed' , {
425- ...details ,
426- parts : partsByFault . malformed . slice ( 0 , MAX_REPORTED_PROVENANCE_FAULT_PARTS ) ,
427- partCount : partsByFault . malformed . length ,
442+ parts : parts . slice ( 0 , MAX_REPORTED_PROVENANCE_FAULT_PARTS ) ,
443+ partCount : parts . length ,
428444 } )
429445 }
430446}
@@ -467,7 +483,10 @@ export async function projectExecutionDataForDisplay(
467483 return projectLegacyExecutionDataForDisplay ( executionData )
468484 }
469485
470- const registry = await importResolvedSecretTraceRegistry ( provenance , 'traceStore.spanProvenance' )
486+ const provenanceFaults = new Map < string , StoredDisplayProvenanceFault > ( )
487+ const runImport = await importStoredDisplayEnvelope ( provenance , 'traceStore.spanProvenance' )
488+ const registry = runImport . registry
489+ if ( runImport . fault ) provenanceFaults . set ( 'traceSpans' , runImport . fault )
471490
472491 /**
473492 * Compaction drops `executionState`, and with it the only copy of the
@@ -498,11 +517,6 @@ export async function projectExecutionDataForDisplay(
498517
499518 const projectionStore = createReadOnlyProjectionStore ( context )
500519
501- const provenanceFaults = new Map < string , StoredDisplayProvenanceFault > ( )
502- if ( isIncompleteStoredEnvelope ( provenance ) ) {
503- provenanceFaults . set ( 'traceSpans' , 'incomplete' )
504- }
505-
506520 const exactValueProjections = new Map < string , unknown > ( )
507521 for ( const [ valueKey , provenanceKey ] of Object . entries ( EXACT_LOG_VALUE_PROVENANCE_KEYS ) ) {
508522 if (
@@ -513,18 +527,18 @@ export async function projectExecutionDataForDisplay(
513527 continue
514528 }
515529
516- const exactProvenance = executionState [ provenanceKey ]
517- const exactRegistry = isResolvedSecretTraceProvenanceV1 ( exactProvenance )
518- ? new ResolvedSecretTraceRegistry ( [ ] , exactProvenance . scope , { staged : true } )
519- : new ResolvedSecretTraceRegistry ( [ ] , undefined , { staged : true } )
520- if ( isResolvedSecretTraceProvenanceV1 ( exactProvenance ) ) {
521- if ( ! exactProvenance . complete ) provenanceFaults . set ( valueKey , 'incomplete' )
522- await exactRegistry . importProvenance ( exactProvenance , {
523- trusted : true ,
524- origin : 'traceStore.exactProvenance' ,
525- } )
526- } else {
527- provenanceFaults . set ( valueKey , 'malformed' )
530+ const exactImport = await importStoredDisplayEnvelope (
531+ executionState [ provenanceKey ] ,
532+ 'traceStore.exactProvenance'
533+ )
534+ if ( exactImport . fault ) provenanceFaults . set ( valueKey , exactImport . fault )
535+ /**
536+ * The exact value must project against SOME registry, so an unusable envelope gets a latched
537+ * one — the projection then withholds the value rather than passing it through unguarded.
538+ */
539+ let exactRegistry = exactImport . registry
540+ if ( ! exactRegistry ) {
541+ exactRegistry = new ResolvedSecretTraceRegistry ( [ ] , undefined , { staged : true } )
528542 exactRegistry . markIncomplete ( 'untrusted-provenance' , { origin : 'traceStore.exactProvenance' } )
529543 }
530544
0 commit comments