2026-07-17 08:37:52 +00:00
package pipeline
import (
"context"
"errors"
"fmt"
"path"
"sort"
"strings"
"sync"
"time"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
type laneExtractState struct {
index int
prepared preparedLaneExecutor
deps [ ] CheckpointFingerprint
decision CheckpointDecision
values [ ] erasedExtractArtifact
serialized [ ] CheckpointArtifact
warnings [ ] contracts . Warning
rejected [ ] contracts . RejectedOutput
results map [ int ] extractJobResult
remaining int
failed bool
2026-07-22 02:27:19 +00:00
terminal bool
output RunOutput
2026-07-17 08:37:52 +00:00
}
2026-07-17 14:05:21 +00:00
type finalizedExtractResults struct {
accepted [ ] erasedExtractArtifact
serialized [ ] CheckpointArtifact
warnings [ ] contracts . Warning
rejected [ ] contracts . RejectedOutput
decision CheckpointDecision
}
2026-07-21 21:59:48 +00:00
func loadExtract ( loader CheckpointLoader , stepID , laneID , moduleKey string , deps [ ] CheckpointFingerprint ) ( ExtractCheckpoint , CheckpointDecision ) {
if stepAware , ok := loader . ( StepCheckpointLoader ) ; ok {
return stepAware . ExtractForStep ( stepID , laneID , moduleKey , deps )
}
2026-07-17 14:05:21 +00:00
return loader . Extract ( laneID , moduleKey , deps )
}
2026-07-21 21:59:48 +00:00
func recordExtract ( recorder CheckpointRecorder , stepID , laneID , moduleKey string , deps [ ] CheckpointFingerprint , outputs [ ] CheckpointArtifact , rejected [ ] contracts . RejectedOutput , warnings [ ] contracts . Warning ) error {
return checkpointExtractSucceeded ( recorder , stepID , laneID , moduleKey , deps , outputs , rejected , warnings )
2026-07-17 14:05:21 +00:00
}
2026-07-17 08:37:52 +00:00
type extractJob struct {
lane * laneExtractState
chunk source . Chunk
}
type extractJobResult struct {
laneIndex int
chunkIndex int
value erasedExtractArtifact
serialized CheckpointArtifact
warnings [ ] contracts . Warning
rejected * contracts . RejectedOutput
err error
}
type laneCompletion struct {
index int
output RunOutput
err error
}
type orderedRunError struct {
stage int
lane int
chunk int
err error
}
2026-07-21 21:19:00 +00:00
func ( r * Runner ) runLanes ( parent context . Context , input RunInput , step PreparedPipelineStep , checkpoints CheckpointRecorder , loader CheckpointLoader , doc * source . SourceDocument , sourceInput contracts . LLMInputMaterial , sessionID string , chunks [ ] source . Chunk ) ( RunOutput , error ) {
2026-07-17 08:37:52 +00:00
output := RunOutput { Manifest : manifestFromPipeline ( input ) }
2026-07-22 02:03:27 +00:00
states , err := initializeLaneStates ( input , step , checkpoints , loader , doc , chunks , & output )
if err != nil {
return output , err
}
completedOutputs , runErrors := r . runLaneEngine ( parent , input , checkpoints , loader , doc , sourceInput , sessionID , chunks , states )
if err := mergeCompletedLanes ( & output , completedOutputs ) ; err != nil {
return output , err
}
if err := selectRunError ( parent , runErrors ) ; err != nil {
return output , err
}
return output , nil
}
func initializeLaneStates ( input RunInput , step PreparedPipelineStep , checkpoints CheckpointRecorder , loader CheckpointLoader , doc * source . SourceDocument , chunks [ ] source . Chunk , output * RunOutput ) ( [ ] * laneExtractState , error ) {
2026-07-21 21:19:00 +00:00
states := make ( [ ] * laneExtractState , len ( step . lanes ) )
for i , prepared := range step . lanes {
2026-07-17 08:37:52 +00:00
if prepared . typed == nil {
2026-07-22 02:03:27 +00:00
return nil , fmt . Errorf ( "typed lane %q executor is not prepared" , prepared . resolved . ID )
2026-07-17 08:37:52 +00:00
}
2026-07-22 02:03:27 +00:00
if err := setTypedLaneManifestMetadata ( output , prepared . resolved . ID , prepared . typed . extractor , prepared . typed . merger , prepared . typed . normalizer ) ; err != nil {
return nil , err
2026-07-17 23:16:16 -05:00
}
2026-07-22 02:27:19 +00:00
if input . CheckpointPolicy . requiresReusable ( input . stepID , prepared . resolved . ID ) && ! input . CheckpointPolicy . forced ( input . stepID , prepared . resolved . ID ) {
state , err := hydrateRequiredLane ( input , loader , doc , i , prepared , output )
if err != nil {
return nil , err
}
states [ i ] = state
continue
}
2026-07-22 02:13:50 +00:00
state , err := prepareLaneExtract ( input , loader , doc , chunks , i , prepared , output )
2026-07-17 08:37:52 +00:00
if err != nil {
2026-07-22 02:03:27 +00:00
return nil , err
2026-07-17 08:37:52 +00:00
}
if ! state . decision . Reused {
2026-07-21 21:59:48 +00:00
if err := checkpointExtractRunning ( checkpoints , input . stepID , prepared . resolved . ID , prepared . resolved . Extract . Module , state . deps ) ; err != nil {
2026-07-22 02:03:27 +00:00
return nil , fmt . Errorf ( "write extract checkpoint for lane %q: %w" , prepared . resolved . ID , err )
2026-07-17 08:37:52 +00:00
}
2026-07-21 21:59:48 +00:00
} else if err := finalizeLaneExtract ( checkpoints , input . stepID , state ) ; err != nil {
2026-07-22 02:03:27 +00:00
return nil , err
2026-07-17 08:37:52 +00:00
}
states [ i ] = state
}
2026-07-22 02:03:27 +00:00
return states , nil
}
2026-07-17 08:37:52 +00:00
2026-07-22 02:03:27 +00:00
func ( r * Runner ) runLaneEngine ( parent context . Context , input RunInput , checkpoints CheckpointRecorder , loader CheckpointLoader , doc * source . SourceDocument , sourceInput contracts . LLMInputMaterial , sessionID string , chunks [ ] source . Chunk , states [ ] * laneExtractState ) ( [ ] RunOutput , [ ] orderedRunError ) {
2026-07-17 08:37:52 +00:00
workerCount := input . ExtractWorkers
if workerCount < 1 {
workerCount = 1
}
ctx , cancel := context . WithCancel ( parent )
defer cancel ( )
jobs := make ( chan extractJob , workerCount )
results := make ( chan extractJobResult , workerCount )
completions := make ( chan laneCompletion , len ( states ) )
2026-07-17 08:49:08 +00:00
continuations := make ( chan * laneExtractState , workerCount )
2026-07-17 08:37:52 +00:00
var workers sync . WaitGroup
for i := 0 ; i < workerCount ; i ++ {
workers . Add ( 1 )
go func ( ) {
defer workers . Done ( )
for job := range jobs {
if ctx . Err ( ) != nil {
continue
}
result := r . runExtractJob ( ctx , input , doc , sourceInput , sessionID , job )
results <- result
}
} ( )
}
go func ( ) {
defer close ( jobs )
for chunkIndex := range chunks {
for laneIndex := range states {
state := states [ laneIndex ]
2026-07-22 02:27:19 +00:00
if state . terminal || state . decision . Reused {
2026-07-17 08:37:52 +00:00
continue
}
select {
case jobs <- extractJob { lane : state , chunk : chunks [ chunkIndex ] } :
case <- ctx . Done ( ) :
return
}
}
}
} ( )
go func ( ) { workers . Wait ( ) ; close ( results ) } ( )
2026-07-17 08:49:08 +00:00
var continuationWorkers sync . WaitGroup
for i := 0 ; i < workerCount ; i ++ {
continuationWorkers . Add ( 1 )
go func ( ) {
defer continuationWorkers . Done ( )
for state := range continuations {
if err := ctx . Err ( ) ; err != nil {
completions <- laneCompletion { index : state . index , err : err }
continue
}
laneOutput , err := r . continueLane ( ctx , input , checkpoints , loader , doc , sourceInput , sessionID , chunks , state )
completions <- laneCompletion { index : state . index , output : laneOutput , err : err }
}
} ( )
}
2026-07-17 08:37:52 +00:00
2026-07-22 02:13:50 +00:00
completedOutputs , runErrors := collectLaneResults ( ctx , cancel , input , checkpoints , chunks , states , results , completions , continuations )
close ( continuations )
continuationWorkers . Wait ( )
return completedOutputs , runErrors
}
func collectLaneResults ( ctx context . Context , cancel context . CancelFunc , input RunInput , checkpoints CheckpointRecorder , chunks [ ] source . Chunk , states [ ] * laneExtractState , results <- chan extractJobResult , completions <- chan laneCompletion , continuations chan <- * laneExtractState ) ( [ ] RunOutput , [ ] orderedRunError ) {
2026-07-17 08:37:52 +00:00
completedOutputs := make ( [ ] RunOutput , len ( states ) )
var runErrors [ ] orderedRunError
2026-07-17 08:49:08 +00:00
var pendingContinuations [ ] * laneExtractState
2026-07-17 08:37:52 +00:00
launched , completed := 0 , 0
for _ , state := range states {
2026-07-22 02:27:19 +00:00
if state . terminal {
completedOutputs [ state . index ] = state . output
} else if state . decision . Reused {
2026-07-17 08:49:08 +00:00
pendingContinuations = append ( pendingContinuations , state )
2026-07-17 08:37:52 +00:00
}
}
resultChannel := results
2026-07-17 08:49:08 +00:00
for resultChannel != nil || len ( pendingContinuations ) > 0 || completed < launched {
var continuationChannel chan <- * laneExtractState
var nextContinuation * laneExtractState
if len ( pendingContinuations ) > 0 && ctx . Err ( ) == nil {
continuationChannel = continuations
nextContinuation = pendingContinuations [ 0 ]
} else if ctx . Err ( ) != nil {
pendingContinuations = nil
}
2026-07-17 08:37:52 +00:00
select {
2026-07-17 08:49:08 +00:00
case continuationChannel <- nextContinuation :
pendingContinuations = pendingContinuations [ 1 : ]
launched ++
2026-07-17 08:37:52 +00:00
case result , ok := <- resultChannel :
if ! ok {
resultChannel = nil
continue
}
state := states [ result . laneIndex ]
state . remaining --
if result . err != nil {
state . failed = true
runErrors = append ( runErrors , orderedRunError { stage : 0 , lane : state . index , chunk : result . chunkIndex , err : result . err } )
2026-07-21 21:59:48 +00:00
_ = checkpointExtractFailed ( checkpoints , input . stepID , state . prepared . resolved . ID , state . prepared . resolved . Extract . Module , state . deps , result . err )
2026-07-17 08:37:52 +00:00
cancel ( )
} else {
state . results [ result . chunkIndex ] = result
}
if state . remaining == 0 && ! state . failed && ctx . Err ( ) == nil {
2026-07-21 21:59:48 +00:00
if err := finalizeLaneExtract ( checkpoints , input . stepID , state ) ; err != nil {
2026-07-17 08:37:52 +00:00
state . failed = true
runErrors = append ( runErrors , orderedRunError { stage : 0 , lane : state . index , chunk : len ( chunks ) , err : err } )
cancel ( )
} else {
2026-07-17 08:49:08 +00:00
pendingContinuations = append ( pendingContinuations , state )
2026-07-17 08:37:52 +00:00
}
}
case completion := <- completions :
completed ++
completedOutputs [ completion . index ] = completion . output
if completion . err != nil {
runErrors = append ( runErrors , classifyLaneError ( completion . index , len ( chunks ) , completion . err ) )
cancel ( )
}
}
}
2026-07-22 02:03:27 +00:00
return completedOutputs , runErrors
}
2026-07-22 02:27:19 +00:00
func hydrateRequiredLane ( input RunInput , loader CheckpointLoader , doc * source . SourceDocument , index int , prepared preparedLaneExecutor , output * RunOutput ) ( * laneExtractState , error ) {
lane , typed := prepared . resolved , prepared . typed
local := RunOutput { Manifest : manifestFromPipeline ( input ) }
checkpoint , decision := loader . AcceptedNormalize ( input . stepID , lane . ID , lane . Normalize . Module )
if decision . Reused {
decision = checkpointDecision ( CheckpointDecisionReused , CheckpointReasonAcceptedArtifactReused , "accepted normalized artifact is reusable" )
if checkpoint . Output . LaneID != lane . ID || checkpoint . Output . ModuleKey != lane . Normalize . Module || checkpoint . Output . SourceID != doc . ID {
decision = checkpointDecision ( CheckpointDecisionExecuted , CheckpointReasonArtifactPayloadInvalid , "accepted normalized artifact provenance does not match the producer lane" )
}
}
decision , err := resolveCheckpointDecision ( & local , loader , input . CheckpointPolicy , StageNormalize , input . stepID , lane . ID , lane . Normalize . Module , decision , typed . codec , [ ] CheckpointArtifact { checkpoint . Output } )
if err != nil {
if mergeErr := mergeLaneOutput ( output , local ) ; mergeErr != nil {
return nil , mergeErr
}
return nil , err
}
_ , hydrated , err := decodeCanonicalCheckpointArtifact ( typed . codec , checkpoint . Output )
if err != nil {
return nil , fmt . Errorf ( "hydrate accepted normalized artifact for step %q lane %q: %w" , input . stepID , lane . ID , err )
}
local . Warnings = append ( local . Warnings , cloneWarnings ( checkpoint . Warnings ) ... )
local . NormalizeOutputs = append ( local . NormalizeOutputs , contracts . SerializedOutput {
StepID : input . stepID ,
LaneID : lane . ID ,
NormalizerKey : lane . Normalize . Module ,
SourceID : doc . ID ,
Artifact : contracts . CloneSerializedArtifact ( hydrated . Artifact ) ,
} )
return & laneExtractState { index : index , prepared : prepared , decision : decision , terminal : true , output : local } , nil
}
2026-07-22 02:03:27 +00:00
func mergeCompletedLanes ( output * RunOutput , completedOutputs [ ] RunOutput ) error {
2026-07-17 08:37:52 +00:00
for i := range completedOutputs {
2026-07-22 02:03:27 +00:00
if err := mergeLaneOutput ( output , completedOutputs [ i ] ) ; err != nil {
return err
2026-07-17 23:16:16 -05:00
}
2026-07-17 08:37:52 +00:00
}
2026-07-22 02:03:27 +00:00
return nil
2026-07-17 08:37:52 +00:00
}
2026-07-22 02:13:50 +00:00
func prepareLaneExtract ( input RunInput , loader CheckpointLoader , doc * source . SourceDocument , chunks [ ] source . Chunk , index int , prepared preparedLaneExecutor , output * RunOutput ) ( * laneExtractState , error ) {
2026-07-17 08:37:52 +00:00
lane , typed := prepared . resolved , prepared . typed
digest , err := joinedChunkDigest ( chunks )
if err != nil {
return nil , fmt . Errorf ( "digest chunks for lane %q: %w" , lane . ID , err )
}
2026-07-21 21:39:23 +00:00
extractReferences := operationReferenceSet ( input , lane . ExtractReferences )
deps := append ( digestFingerprints ( "chunks" , digest ) , generatedReferenceDependencies ( extractReferences ) ... )
state := & laneExtractState { index : index , prepared : prepared , deps : normalizeCheckpointFingerprints ( deps ) , remaining : len ( chunks ) , results : make ( map [ int ] extractJobResult , len ( chunks ) ) }
2026-07-21 21:59:48 +00:00
cp , decision := loadExtract ( loader , input . stepID , lane . ID , lane . Extract . Module , state . deps )
2026-07-22 02:13:50 +00:00
decision , err = resolveCheckpointDecision ( output , loader , input . CheckpointPolicy , StageExtract , input . stepID , lane . ID , lane . Extract . Module , decision , typed . codec , cp . Outputs )
2026-07-22 02:03:27 +00:00
if err != nil {
2026-07-21 21:59:48 +00:00
return nil , err
}
2026-07-17 08:37:52 +00:00
state . decision = decision
if decision . Reused {
state . remaining = 0
for _ , stored := range cp . Outputs {
2026-07-21 21:59:48 +00:00
value , hydrated , decodeErr := decodeCanonicalCheckpointArtifact ( typed . codec , stored )
2026-07-17 08:37:52 +00:00
if decodeErr != nil {
return nil , fmt . Errorf ( "decode extract checkpoint for lane %q: %w" , lane . ID , decodeErr )
}
2026-07-21 21:59:48 +00:00
stored = hydrated
2026-07-17 08:37:52 +00:00
artifact := erasedExtractArtifact { LaneID : lane . ID , ExtractorKey : lane . Extract . Module , SourceID : doc . ID , ChunkID : stored . ChunkID , ChunkIndex : stored . ChunkIndex , ChunkRef : stored . ChunkRef , Value : value }
if stored . ChunkIndex >= 0 && stored . ChunkIndex < len ( chunks ) && artifact . ChunkRef == ( source . SourceRef { } ) {
artifact . ChunkRef = chunks [ stored . ChunkIndex ] . Ref
}
state . values = append ( state . values , artifact )
state . serialized = append ( state . serialized , cloneCheckpointArtifact ( stored ) )
}
state . warnings , state . rejected = cloneWarnings ( cp . Warnings ) , cloneRejectedOutputs ( cp . Rejected )
}
return state , nil
}
func ( r * Runner ) runExtractJob ( ctx context . Context , input RunInput , doc * source . SourceDocument , sourceInput contracts . LLMInputMaterial , sessionID string , job extractJob ) extractJobResult {
2026-07-17 23:16:16 -05:00
state := job . lane
chunk , cloneErr := cloneSourceChunk ( job . chunk )
2026-07-17 08:37:52 +00:00
lane , typed := state . prepared . resolved , state . prepared . typed
2026-07-17 23:16:16 -05:00
result := extractJobResult { laneIndex : state . index , chunkIndex : job . chunk . Index }
if cloneErr != nil {
result . err = fmt . Errorf ( "clone chunk %q for extraction: %w" , job . chunk . ID , cloneErr )
return result
}
2026-07-17 08:37:52 +00:00
var accepted erasedExtractArtifact
var serialized CheckpointArtifact
var acceptedWarnings [ ] contracts . Warning
ok , rejection , err := runWithRetry ( ctx , lane . Extract . Retries , func ( attempt int ) ( bool , * contracts . RejectedOutput , error ) {
started := time . Now ( ) . UTC ( )
attemptPath := path . Join ( "extract" , debugPathComponent ( lane . ID ) , fmt . Sprintf ( "chunk-%06d" , chunk . Index + 1 ) , fmt . Sprintf ( "attempt-%02d" , attempt ) )
attemptCtx , llmScope := withDebugLLMScope ( ctx , attemptPath )
2026-07-21 21:19:00 +00:00
terminal := newAttemptTerminalRecorder ( input . Debug , attemptPath , "extract" , llmScope , debugTimedEnvelope { Stage : string ( StageExtract ) , StepID : input . stepID , LaneID : lane . ID , ModuleKey : lane . Extract . Module , Attempt : attempt , StartedAt : started } )
2026-07-17 23:16:16 -05:00
requestMetadata , metadataErr := cloneMetadata ( input . Metadata )
if metadataErr != nil {
return false , nil , terminal . record ( nil , fmt . Errorf ( "clone extract request metadata: %w" , metadataErr ) )
}
2026-07-21 21:39:23 +00:00
extractReferences := operationReferenceSet ( input , lane . ExtractReferences )
extracted , callErr := typed . extract ( attemptCtx , typed . extractor , contracts . TypedExtractionRequest { Source : doc , Chunk : & chunk , SourceInput : chunkInputMaterial ( sourceInput , chunk ) , SessionID : sessionID , References : CloneReferenceSet ( extractReferences ) , LLMProfile : lane . Extract . LLMProfile , Metadata : requestMetadata } )
2026-07-17 08:37:52 +00:00
if callErr != nil {
2026-07-17 15:23:27 +00:00
attemptErr := fmt . Errorf ( "extract lane %q chunk %q with extractor %q: %w" , lane . ID , chunk . ID , lane . Extract . Module , callErr )
return false , nil , terminal . record ( nil , attemptErr )
2026-07-17 08:37:52 +00:00
}
artifact := erasedExtractArtifact { LaneID : lane . ID , ExtractorKey : lane . Extract . Module , SourceID : doc . ID , ChunkID : chunk . ID , ChunkIndex : chunk . Index , ChunkRef : chunk . Ref , Value : extracted . Value }
2026-07-17 15:23:27 +00:00
attemptWarnings := cloneWarnings ( extracted . Warnings )
serializedCandidate , encodeErr := serializeCandidateArtifact ( typed . codec , artifact . LaneID , artifact . ExtractorKey , artifact . SourceID , artifact . Value )
if encodeErr != nil {
attemptErr := fmt . Errorf ( "serialize extract candidate for lane %q chunk %q: %w" , lane . ID , chunk . ID , encodeErr )
payload := map [ string ] any { "warnings" : debugWarningEnvelopes ( attemptWarnings ) }
return false , nil , terminal . record ( payload , attemptErr )
}
serializedCandidate . ChunkID , serializedCandidate . ChunkIndex , serializedCandidate . ChunkRef = artifact . ChunkID , artifact . ChunkIndex , artifact . ChunkRef
2026-07-21 21:39:23 +00:00
warnings , rejected , validateErr := r . validateTypedArtifact ( attemptCtx , typed . codec , typedValidationTarget { stage : StageExtract , stepID : input . stepID , laneID : lane . ID , moduleKey : lane . Extract . Module , source : doc , sourceID : doc . ID , sourceInput : chunkInputMaterial ( sourceInput , chunk ) , sessionID : sessionID , references : extractReferences , metadata : input . Metadata , chunk : & chunk , ref : chunk . Ref , value : extracted . Value , candidate : & serializedCandidate } , state . prepared . extractValidators , attempt , input . Debug )
2026-07-17 15:23:27 +00:00
attemptWarnings = append ( attemptWarnings , warnings ... )
payload := map [ string ] any { "output" : debugCheckpointArtifact ( serializedCandidate ) , "warnings" : debugWarningEnvelopes ( attemptWarnings ) , "rejection" : debugRejectedOutputPtr ( rejected ) }
2026-07-17 08:37:52 +00:00
if validateErr != nil || rejected != nil {
2026-07-17 15:23:27 +00:00
return false , rejected , terminal . record ( payload , validateErr )
2026-07-17 08:37:52 +00:00
}
stored , encodeErr := checkpointArtifact ( typed . codec , artifact . LaneID , artifact . ExtractorKey , artifact . SourceID , artifact . Value )
if encodeErr != nil {
2026-07-17 15:23:27 +00:00
attemptErr := fmt . Errorf ( "serialize accepted extract output for lane %q chunk %q: %w" , lane . ID , chunk . ID , encodeErr )
return false , nil , terminal . record ( payload , attemptErr )
2026-07-17 08:37:52 +00:00
}
stored . ChunkID , stored . ChunkIndex , stored . ChunkRef = artifact . ChunkID , artifact . ChunkIndex , artifact . ChunkRef
accepted , serialized = artifact , stored
2026-07-17 15:23:27 +00:00
acceptedWarnings = attemptWarnings
if debugErr := terminal . record ( payload , nil ) ; debugErr != nil {
2026-07-17 08:37:52 +00:00
return false , nil , debugErr
}
return true , nil , nil
} )
result . err = err
if err == nil && ! ok {
result . rejected = rejection
return result
}
result . value , result . serialized , result . warnings = accepted , serialized , acceptedWarnings
return result
}
2026-07-21 21:59:48 +00:00
func finalizeLaneExtract ( checkpoints CheckpointRecorder , stepID string , state * laneExtractState ) error {
2026-07-17 08:37:52 +00:00
lane := state . prepared . resolved
indexes := make ( [ ] int , 0 , len ( state . results ) )
for index := range state . results {
indexes = append ( indexes , index )
}
sort . Ints ( indexes )
for _ , index := range indexes {
result := state . results [ index ]
if result . rejected != nil {
state . rejected = append ( state . rejected , * result . rejected )
continue
}
state . values = append ( state . values , result . value )
state . serialized = append ( state . serialized , result . serialized )
state . warnings = append ( state . warnings , result . warnings ... )
}
sort . SliceStable ( state . values , func ( i , j int ) bool { return state . values [ i ] . ChunkIndex < state . values [ j ] . ChunkIndex } )
sort . SliceStable ( state . serialized , func ( i , j int ) bool { return state . serialized [ i ] . ChunkIndex < state . serialized [ j ] . ChunkIndex } )
sort . SliceStable ( state . rejected , func ( i , j int ) bool { return state . rejected [ i ] . ChunkIndex < state . rejected [ j ] . ChunkIndex } )
if ! state . decision . Reused {
2026-07-21 21:59:48 +00:00
if err := recordExtract ( checkpoints , stepID , lane . ID , lane . Extract . Module , state . deps , state . serialized , state . rejected , state . warnings ) ; err != nil {
2026-07-17 08:37:52 +00:00
return fmt . Errorf ( "write extract checkpoint for lane %q: %w" , lane . ID , err )
}
}
return nil
}
func ( r * Runner ) continueLane ( ctx context . Context , input RunInput , checkpoints CheckpointRecorder , loader CheckpointLoader , doc * source . SourceDocument , sourceInput contracts . LLMInputMaterial , sessionID string , chunks [ ] source . Chunk , state * laneExtractState ) ( RunOutput , error ) {
lane := state . prepared . resolved
local := RunOutput { Manifest : manifestFromPipeline ( input ) }
2026-07-17 14:05:21 +00:00
results := finalizedExtractResults {
accepted : state . values ,
serialized : state . serialized ,
warnings : state . warnings ,
rejected : state . rejected ,
decision : state . decision ,
}
local . Warnings = append ( local . Warnings , cloneWarnings ( results . warnings ) ... )
local . Rejected = append ( local . Rejected , cloneRejectedOutputs ( results . rejected ) ... )
2026-07-21 21:19:00 +00:00
if err := writeDebugTimed ( input . Debug , path . Join ( "extract" , debugPathComponent ( lane . ID ) , "input.json" ) , debugTimedEnvelope { Stage : string ( StageExtract ) , StepID : input . stepID , LaneID : lane . ID , ModuleKey : lane . Extract . Module , StartedAt : time . Now ( ) . UTC ( ) , Payload : map [ string ] any { "reused" : results . decision . Reused , "decision" : results . decision , "source" : debugSourceDocumentEnvelope ( doc ) , "chunks" : debugSourceChunkEnvelopes ( chunks ) , "options" : redactSensitiveMap ( lane . Extract . Options ) , "metadata" : redactSensitiveMap ( input . Metadata ) } } ) ; err != nil {
2026-07-17 14:05:21 +00:00
return local , & laneRunError { stage : StageExtract , err : err }
}
2026-07-21 21:19:00 +00:00
if err := writeDebugTimed ( input . Debug , path . Join ( "extract" , debugPathComponent ( lane . ID ) , "output.json" ) , debugTimedEnvelope { Stage : string ( StageExtract ) , StepID : input . stepID , LaneID : lane . ID , ModuleKey : lane . Extract . Module , StartedAt : time . Now ( ) . UTC ( ) , Payload : map [ string ] any { "reused" : results . decision . Reused , "outputs" : debugCheckpointArtifacts ( results . serialized ) , "rejected" : debugRejectedOutputEnvelopes ( results . rejected ) , "warnings" : debugWarningEnvelopes ( results . warnings ) } } ) ; err != nil {
2026-07-17 14:05:21 +00:00
return local , & laneRunError { stage : StageExtract , err : err }
}
if len ( results . accepted ) == 0 {
return local , nil
}
err := r . continueTypedLane ( ctx , input , checkpoints , loader , doc , sourceInput , sessionID , state . prepared , results , & local )
2026-07-17 08:37:52 +00:00
return local , err
}
func classifyLaneError ( lane , sentinel int , err error ) orderedRunError {
stage := 1
var laneErr * laneRunError
if errors . As ( err , & laneErr ) {
switch laneErr . stage {
case StageExtract :
stage = 0
case StageNormalize :
stage = 2
}
} else if strings . Contains ( strings . ToLower ( err . Error ( ) ) , "normalize" ) {
stage = 2
}
return orderedRunError { stage : stage , lane : lane , chunk : sentinel , err : err }
}
func selectRunError ( parent context . Context , values [ ] orderedRunError ) error {
if err := parent . Err ( ) ; err != nil {
return err
}
if len ( values ) == 0 {
return nil
}
hasReal := false
for _ , value := range values {
if ! errors . Is ( value . err , context . Canceled ) && ! errors . Is ( value . err , context . DeadlineExceeded ) {
hasReal = true
break
}
}
filtered := values [ : 0 ]
for _ , value := range values {
if hasReal && ( errors . Is ( value . err , context . Canceled ) || errors . Is ( value . err , context . DeadlineExceeded ) ) {
continue
}
filtered = append ( filtered , value )
}
sort . SliceStable ( filtered , func ( i , j int ) bool {
if filtered [ i ] . stage != filtered [ j ] . stage {
return filtered [ i ] . stage < filtered [ j ] . stage
}
if filtered [ i ] . lane != filtered [ j ] . lane {
return filtered [ i ] . lane < filtered [ j ] . lane
}
return filtered [ i ] . chunk < filtered [ j ] . chunk
} )
return filtered [ 0 ] . err
}
2026-07-17 23:16:16 -05:00
func mergeLaneOutput ( dst * RunOutput , src RunOutput ) error {
2026-07-17 08:37:52 +00:00
if dst == nil {
2026-07-17 23:16:16 -05:00
return nil
2026-07-17 08:37:52 +00:00
}
dst . NormalizeOutputs = append ( dst . NormalizeOutputs , cloneSerializedOutputs ( src . NormalizeOutputs ) ... )
dst . Rejected = append ( dst . Rejected , cloneRejectedOutputs ( src . Rejected ) ... )
dst . Warnings = append ( dst . Warnings , cloneWarnings ( src . Warnings ) ... )
dst . CheckpointEvents = append ( dst . CheckpointEvents , src . CheckpointEvents ... )
for i := range dst . Manifest . ArtifactLanes {
for j := range src . Manifest . ArtifactLanes {
2026-07-21 21:39:23 +00:00
if dst . Manifest . ArtifactLanes [ i ] . ID == src . Manifest . ArtifactLanes [ j ] . ID && dst . Manifest . ArtifactLanes [ i ] . StepID == src . Manifest . ArtifactLanes [ j ] . StepID && src . Manifest . ArtifactLanes [ j ] . Metadata != nil {
2026-07-17 23:16:16 -05:00
metadata , err := cloneMetadata ( src . Manifest . ArtifactLanes [ j ] . Metadata )
if err != nil {
return fmt . Errorf ( "clone lane %q manifest metadata: %w" , dst . Manifest . ArtifactLanes [ i ] . ID , err )
}
dst . Manifest . ArtifactLanes [ i ] . Metadata = metadata
2026-07-17 08:37:52 +00:00
}
}
}
2026-07-17 23:16:16 -05:00
return nil
2026-07-17 08:37:52 +00:00
}