2026-07-17 08:37:52 +00:00
package pipeline
import (
"context"
"errors"
"fmt"
"path"
"sort"
"strings"
"sync"
"time"
2026-08-27 01:27:25 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
2026-08-09 00:41:28 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/fileio"
2026-07-17 08:37:52 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
type laneExtractState struct {
2026-08-27 01:27:25 +00:00
index int
prepared preparedLaneExecutor
deps [ ] CheckpointFingerprint
decision CheckpointDecision
2026-08-27 11:50:37 +00:00
reuseEligible bool
2026-08-27 01:27:25 +00:00
values [ ] erasedExtractArtifact
serialized [ ] CheckpointArtifact
2026-08-27 15:29:10 +00:00
diagnostics [ ] contracts . DiagnosticGroup
2026-08-27 01:27:25 +00:00
rejected [ ] contracts . RejectedOutput
incomplete [ ] int
validationSummaries [ ] artifacts . ValidationSummary
results map [ int ] extractJobResult
remaining int
failed bool
terminal bool
output RunOutput
2026-07-17 08:37:52 +00:00
}
2026-07-17 14:05:21 +00:00
type finalizedExtractResults struct {
2026-08-27 01:27:25 +00:00
accepted [ ] erasedExtractArtifact
serialized [ ] CheckpointArtifact
2026-08-27 15:29:10 +00:00
diagnostics [ ] contracts . DiagnosticGroup
2026-08-27 01:27:25 +00:00
rejected [ ] contracts . RejectedOutput
incomplete [ ] int
validationSummaries [ ] artifacts . ValidationSummary
decision CheckpointDecision
2026-08-27 11:50:37 +00:00
reuseEligible bool
2026-07-17 14:05:21 +00:00
}
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-08-27 16:40:17 +00:00
func recordExtract ( recorder CheckpointRecorder , stepID , laneID , moduleKey string , deps [ ] CheckpointFingerprint , outputs [ ] CheckpointArtifact , rejected [ ] contracts . RejectedOutput ) error {
return checkpointExtractSucceeded ( recorder , stepID , laneID , moduleKey , deps , outputs , rejected )
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 {
2026-08-27 01:06:17 +00:00
laneIndex int
chunkIndex int
value erasedExtractArtifact
serialized CheckpointArtifact
2026-08-27 15:29:10 +00:00
diagnostics [ ] contracts . DiagnosticGroup
2026-08-27 01:06:17 +00:00
rejected * contracts . RejectedOutput
validationIncomplete bool
2026-08-27 01:27:25 +00:00
validationSummary * artifacts . ValidationSummary
2026-08-27 01:06:17 +00:00
err error
}
type extractAttemptValue struct {
artifact erasedExtractArtifact
2026-07-17 08:37:52 +00:00
serialized CheckpointArtifact
2026-08-27 01:06:17 +00:00
terminal * attemptTerminalRecorder
2026-07-17 08:37:52 +00:00
}
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 {
2026-07-22 14:24:03 +00:00
if mergeErr := mergeTerminalLaneStates ( & output , states ) ; mergeErr != nil {
return output , errors . Join ( err , mergeErr )
}
2026-07-22 02:03:27 +00:00
return output , err
}
2026-07-22 14:24:03 +00:00
completedOutputs , runErrors := r . runLaneEngine ( parent , laneEngineConfig {
input : input , checkpoints : checkpoints , loader : loader , doc : doc ,
sourceInput : sourceInput , sessionID : sessionID , chunks : chunks , states : states ,
} )
2026-07-22 02:03:27 +00:00
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 14:24:03 +00:00
return states , 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 {
2026-07-22 14:24:03 +00:00
return states , 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 ) {
2026-08-27 11:50:37 +00:00
if ! laneReferencesReuseEligible ( input , prepared . resolved ) {
return states , fmt . Errorf ( "required reusable checkpoint unavailable for step %q lane %q: generated input lineage is validation-incomplete" , input . stepID , prepared . resolved . ID )
}
2026-07-22 14:24:03 +00:00
state , err := hydrateRequiredLane ( input , loader , doc , i , prepared )
states [ i ] = state
2026-07-22 02:27:19 +00:00
if err != nil {
2026-07-22 14:24:03 +00:00
return states , err
2026-07-22 02:27:19 +00:00
}
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 14:24:03 +00:00
return states , err
2026-07-17 08:37:52 +00:00
}
2026-08-27 11:50:37 +00:00
if ! state . decision . Reused && state . reuseEligible {
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 14:24:03 +00:00
return states , 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 14:24:03 +00:00
return states , 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 14:24:03 +00:00
type laneEngineConfig struct {
input RunInput
checkpoints CheckpointRecorder
loader CheckpointLoader
doc * source . SourceDocument
sourceInput contracts . LLMInputMaterial
sessionID string
chunks [ ] source . Chunk
states [ ] * laneExtractState
}
type laneEngine struct {
runner * Runner
laneEngineConfig
ctx context . Context
cancel context . CancelFunc
workerCount int
jobs chan extractJob
results chan extractJobResult
completions chan laneCompletion
continuations chan * laneExtractState
continuationWorkers sync . WaitGroup
pending [ ] * laneExtractState
completedOutputs [ ] RunOutput
runErrors [ ] orderedRunError
launched int
completed int
}
func ( r * Runner ) runLaneEngine ( parent context . Context , config laneEngineConfig ) ( [ ] RunOutput , [ ] orderedRunError ) {
engine := newLaneEngine ( r , parent , config )
return engine . run ( )
}
func newLaneEngine ( r * Runner , parent context . Context , config laneEngineConfig ) * laneEngine {
workerCount := config . input . ExtractWorkers
2026-07-17 08:37:52 +00:00
if workerCount < 1 {
workerCount = 1
}
ctx , cancel := context . WithCancel ( parent )
2026-07-22 14:24:03 +00:00
return & laneEngine {
runner : r , laneEngineConfig : config , ctx : ctx , cancel : cancel , workerCount : workerCount ,
jobs : make ( chan extractJob , workerCount ) , results : make ( chan extractJobResult , workerCount ) ,
completions : make ( chan laneCompletion , len ( config . states ) ) , continuations : make ( chan * laneExtractState , workerCount ) ,
completedOutputs : make ( [ ] RunOutput , len ( config . states ) ) ,
}
}
func ( e * laneEngine ) run ( ) ( [ ] RunOutput , [ ] orderedRunError ) {
defer e . cancel ( )
e . initializeCollection ( )
e . startExtractWorkers ( )
e . startContinuationWorkers ( )
e . collect ( )
close ( e . continuations )
e . continuationWorkers . Wait ( )
return e . completedOutputs , e . runErrors
}
func ( e * laneEngine ) initializeCollection ( ) {
for _ , state := range e . states {
if state . terminal {
e . completedOutputs [ state . index ] = state . output
} else if state . decision . Reused {
e . pending = append ( e . pending , state )
}
}
}
2026-07-17 08:37:52 +00:00
2026-07-22 14:24:03 +00:00
func ( e * laneEngine ) startExtractWorkers ( ) {
2026-07-17 08:37:52 +00:00
var workers sync . WaitGroup
2026-07-22 14:24:03 +00:00
for i := 0 ; i < e . workerCount ; i ++ {
2026-07-17 08:37:52 +00:00
workers . Add ( 1 )
go func ( ) {
defer workers . Done ( )
2026-07-22 14:24:03 +00:00
for job := range e . jobs {
if e . ctx . Err ( ) != nil {
2026-07-17 08:37:52 +00:00
continue
}
2026-07-22 14:24:03 +00:00
e . results <- e . runner . runExtractJob ( e . ctx , e . input , e . doc , e . sourceInput , e . sessionID , job )
2026-07-17 08:37:52 +00:00
}
} ( )
}
2026-07-22 14:24:03 +00:00
go e . dispatchExtractJobs ( )
go func ( ) { workers . Wait ( ) ; close ( e . results ) } ( )
}
func ( e * laneEngine ) dispatchExtractJobs ( ) {
defer close ( e . jobs )
for chunkIndex := range e . chunks {
for laneIndex := range e . states {
state := e . states [ laneIndex ]
if state . terminal || state . decision . Reused {
continue
}
select {
case e . jobs <- extractJob { lane : state , chunk : e . chunks [ chunkIndex ] } :
case <- e . ctx . Done ( ) :
return
2026-07-17 08:37:52 +00:00
}
}
2026-07-22 14:24:03 +00:00
}
}
func ( e * laneEngine ) startContinuationWorkers ( ) {
for i := 0 ; i < e . workerCount ; i ++ {
e . continuationWorkers . Add ( 1 )
2026-07-17 08:49:08 +00:00
go func ( ) {
2026-07-22 14:24:03 +00:00
defer e . continuationWorkers . Done ( )
for state := range e . continuations {
if err := e . ctx . Err ( ) ; err != nil {
e . completions <- laneCompletion { index : state . index , err : err }
2026-07-17 08:49:08 +00:00
continue
}
2026-07-22 14:24:03 +00:00
laneOutput , err := e . runner . continueLane ( e . ctx , e . input , e . checkpoints , e . loader , e . doc , e . sourceInput , e . sessionID , e . chunks , state )
e . completions <- laneCompletion { index : state . index , output : laneOutput , err : err }
2026-07-17 08:49:08 +00:00
}
} ( )
}
2026-07-22 02:13:50 +00:00
}
2026-08-09 01:19:55 +00:00
// collect is the sole owner of pending, launch, and completion accounting. It
// must drain closed extract results and one completion for every launched
// continuation, even after cancellation.
2026-07-22 14:24:03 +00:00
func ( e * laneEngine ) collect ( ) {
resultChannel := ( <- chan extractJobResult ) ( e . results )
for resultChannel != nil || len ( e . pending ) > 0 || e . completed < e . launched {
2026-07-17 08:49:08 +00:00
var continuationChannel chan <- * laneExtractState
var nextContinuation * laneExtractState
2026-07-22 14:24:03 +00:00
if len ( e . pending ) > 0 && e . ctx . Err ( ) == nil {
continuationChannel = e . continuations
nextContinuation = e . pending [ 0 ]
} else if e . ctx . Err ( ) != nil {
e . pending = nil
2026-07-17 08:49:08 +00:00
}
2026-07-17 08:37:52 +00:00
select {
2026-07-17 08:49:08 +00:00
case continuationChannel <- nextContinuation :
2026-07-22 14:24:03 +00:00
e . pending = e . pending [ 1 : ]
e . launched ++
2026-07-17 08:37:52 +00:00
case result , ok := <- resultChannel :
if ! ok {
resultChannel = nil
continue
}
2026-07-22 14:24:03 +00:00
e . handleExtractResult ( result )
case completion := <- e . completions :
e . handleCompletion ( completion )
2026-07-17 08:37:52 +00:00
}
}
2026-07-22 02:03:27 +00:00
}
2026-07-22 14:24:03 +00:00
func ( e * laneEngine ) handleExtractResult ( result extractJobResult ) {
state := e . states [ result . laneIndex ]
state . remaining --
if result . err != nil {
state . failed = true
e . runErrors = append ( e . runErrors , orderedRunError { stage : 0 , lane : state . index , chunk : result . chunkIndex , err : result . err } )
2026-08-27 11:50:37 +00:00
if state . reuseEligible {
_ = checkpointExtractFailed ( e . checkpoints , e . input . stepID , state . prepared . resolved . ID , state . prepared . resolved . Extract . Module , state . deps , result . err )
}
2026-07-22 14:24:03 +00:00
e . cancel ( )
} else {
state . results [ result . chunkIndex ] = result
}
if state . remaining != 0 || state . failed || e . ctx . Err ( ) != nil {
return
}
if err := finalizeLaneExtract ( e . checkpoints , e . input . stepID , state ) ; err != nil {
state . failed = true
e . runErrors = append ( e . runErrors , orderedRunError { stage : 0 , lane : state . index , chunk : len ( e . chunks ) , err : err } )
e . cancel ( )
return
}
e . pending = append ( e . pending , state )
}
func ( e * laneEngine ) handleCompletion ( completion laneCompletion ) {
e . completed ++
e . completedOutputs [ completion . index ] = completion . output
if completion . err != nil {
e . runErrors = append ( e . runErrors , classifyLaneError ( completion . index , len ( e . chunks ) , completion . err ) )
e . cancel ( )
}
}
func hydrateRequiredLane ( input RunInput , loader CheckpointLoader , doc * source . SourceDocument , index int , prepared preparedLaneExecutor ) ( * laneExtractState , error ) {
2026-07-22 02:27:19 +00:00
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 {
2026-07-22 14:24:03 +00:00
decision = checkpointDecision ( CheckpointDecisionReused , CheckpointReasonAcceptedArtifactReused )
2026-07-22 02:27:19 +00:00
if checkpoint . Output . LaneID != lane . ID || checkpoint . Output . ModuleKey != lane . Normalize . Module || checkpoint . Output . SourceID != doc . ID {
2026-07-22 14:24:03 +00:00
decision = checkpointDecision ( CheckpointDecisionExecuted , CheckpointReasonArtifactPayloadInvalid )
2026-07-22 02:27:19 +00:00
}
}
2026-07-22 14:24:03 +00:00
resolution , err := resolveCheckpointDecision ( & local , loader , input . CheckpointPolicy , StageNormalize , input . stepID , lane . ID , lane . Normalize . Module , decision , typed . codec , [ ] CheckpointArtifact { checkpoint . Output } )
decision = resolution . decision
2026-08-27 11:50:37 +00:00
state := & laneExtractState { index : index , prepared : prepared , decision : decision , reuseEligible : true , terminal : true , output : local }
2026-07-22 02:27:19 +00:00
if err != nil {
2026-07-22 14:24:03 +00:00
return state , err
2026-07-22 02:27:19 +00:00
}
2026-07-22 14:24:03 +00:00
hydrated := resolution . artifacts [ 0 ]
2026-08-27 15:40:45 +00:00
diagnostics , diagnosticErr := promoteCheckpointDiagnostics ( hydrated . Diagnostics , contracts . DiagnosticOrigin { Stage : contracts . DiagnosticOriginStageNormalize , StepID : input . stepID , LaneID : lane . ID , ModuleKey : lane . Normalize . Module } , nil )
if diagnosticErr != nil {
return state , fmt . Errorf ( "promote reused accepted diagnostics: %w" , diagnosticErr )
}
appendDiagnosticGroups ( & local , diagnostics )
2026-07-22 02:27:19 +00:00
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 ) ,
} )
2026-08-27 11:50:37 +00:00
local . normalizeReuseEligibility = map [ generatedOutputKey ] bool { generatedOutputKeyFor ( input . stepID , lane . ID ) : true }
2026-07-22 14:24:03 +00:00
state . output = local
return state , nil
}
func mergeTerminalLaneStates ( output * RunOutput , states [ ] * laneExtractState ) error {
for _ , state := range states {
if state == nil || ! state . terminal {
continue
}
if err := mergeLaneOutput ( output , state . output ) ; err != nil {
return err
}
}
return nil
2026-07-22 02:27:19 +00:00
}
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 ) ... )
2026-08-27 11:50:37 +00:00
state := & laneExtractState { index : index , prepared : prepared , deps : normalizeCheckpointFingerprints ( deps ) , reuseEligible : referenceTargetReuseEligible ( input , lane . ExtractReferences ) , remaining : len ( chunks ) , results : make ( map [ int ] extractJobResult , len ( chunks ) ) }
if ! state . reuseEligible {
state . decision = checkpointDecision ( CheckpointDecisionExecuted , CheckpointReasonValidationIncompleteLineage )
recordCheckpointEvent ( output , loader , string ( StageExtract ) , input . stepID , lane . ID , lane . Extract . Module , state . decision )
return state , nil
}
2026-07-21 21:59:48 +00:00
cp , decision := loadExtract ( loader , input . stepID , lane . ID , lane . Extract . Module , state . deps )
2026-07-22 14:24:03 +00:00
resolution , 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-22 14:24:03 +00:00
decision = resolution . decision
2026-07-17 08:37:52 +00:00
state . decision = decision
if decision . Reused {
state . remaining = 0
2026-07-22 14:24:03 +00:00
for i , stored := range resolution . artifacts {
value := resolution . values [ i ]
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 ) )
2026-08-27 15:40:45 +00:00
chunk := source . Chunk { ID : stored . ChunkID , Index : stored . ChunkIndex }
diagnostics , diagnosticErr := promoteCheckpointDiagnostics ( stored . Diagnostics , contracts . DiagnosticOrigin { Stage : contracts . DiagnosticOriginStageExtract , StepID : input . stepID , LaneID : lane . ID , ModuleKey : lane . Extract . Module } , & chunk )
if diagnosticErr != nil {
return nil , fmt . Errorf ( "promote reused extract diagnostics: %w" , diagnosticErr )
}
state . diagnostics = append ( state . diagnostics , diagnostics ... )
2026-07-17 08:37:52 +00:00
}
2026-08-27 16:40:17 +00:00
state . rejected = cloneRejectedOutputs ( cp . Rejected )
2026-07-17 08:37:52 +00:00
}
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-08-27 01:06:17 +00:00
terminalResult , err := runProducerAttempts ( ctx , producerAttemptConfig { Retries : lane . Extract . Retries , Policy : lane . ExtractValidationPolicy , AllowStructuralRetry : lane . ExtractExecutionClass == contracts . ExecutionClassLLMBacked } , func ( attemptCtx context . Context , request producerAttemptRequest ) ( producerAttemptOutput , error ) {
attempt := request . Number
2026-07-17 08:37:52 +00:00
started := time . Now ( ) . UTC ( )
2026-08-09 00:41:28 +00:00
attemptPath := path . Join ( "extract" , fileio . EncodePathComponent ( lane . ID ) , fmt . Sprintf ( "chunk-%06d" , chunk . Index + 1 ) , fmt . Sprintf ( "attempt-%02d" , attempt ) )
2026-08-27 01:06:17 +00:00
attemptCtx , llmScope := withDebugLLMScope ( attemptCtx , attemptPath )
2026-08-27 01:27:25 +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 , AttemptKind : string ( request . Kind ) , StartedAt : started } )
2026-07-17 23:16:16 -05:00
requestMetadata , metadataErr := cloneMetadata ( input . Metadata )
if metadataErr != nil {
2026-08-27 01:06:17 +00:00
return producerAttemptOutput { } , terminal . record ( nil , fmt . Errorf ( "clone extract request metadata: %w" , metadataErr ) )
2026-07-17 23:16:16 -05:00
}
2026-07-21 21:39:23 +00:00
extractReferences := operationReferenceSet ( input , lane . ExtractReferences )
2026-08-27 01:06:17 +00:00
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 , StructuredOutputRepairAttempts : cloneStructuredOutputRepairAttempts ( lane . Extract . StructuredOutputRepairAttempts ) , Correction : request . Correction , 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 )
2026-08-27 01:06:17 +00:00
return producerAttemptOutput { } , 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
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 )
2026-08-27 16:40:17 +00:00
payload := map [ string ] any { }
2026-08-27 01:06:17 +00:00
return producerAttemptOutput { } , terminal . record ( payload , attemptErr )
2026-07-17 15:23:27 +00:00
}
serializedCandidate . ChunkID , serializedCandidate . ChunkIndex , serializedCandidate . ChunkRef = artifact . ChunkID , artifact . ChunkIndex , artifact . ChunkRef
2026-08-27 16:40:17 +00:00
return producerAttemptOutput { Value : extractAttemptValue { artifact : artifact , serialized : serializedCandidate , terminal : & terminal } , Candidate : extracted . ModelCandidate , Diagnostics : contracts . CloneProducerDiagnostics ( extracted . Diagnostics ) } , nil
2026-08-27 01:06:17 +00:00
} , func ( validationCtx context . Context , output producerAttemptOutput ) ( validationReport , error ) {
candidate , ok := output . Value . ( extractAttemptValue )
if ! ok {
return validationReport { } , fmt . Errorf ( "extract attempt has incompatible value" )
2026-08-09 01:19:55 +00:00
}
2026-08-27 01:06:17 +00:00
report , validationErr := r . validateTypedReport ( validationCtx , 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 : operationReferenceSet ( input , lane . ExtractReferences ) , metadata : input . Metadata , chunk : & chunk , ref : chunk . Ref , value : candidate . artifact . Value , candidate : & candidate . serialized } , state . prepared . extractValidators , candidate . terminal . envelope . Attempt , input . Debug )
payload := map [ string ] any {
"output" : debugCheckpointArtifact ( candidate . serialized ) ,
"rejection" : debugRejectedOutputPtr ( typedRejection ( report , typedValidationTarget { stage : StageExtract , stepID : input . stepID , laneID : lane . ID , moduleKey : lane . Extract . Module , chunk : & chunk } , candidate . terminal . envelope . Attempt ) ) ,
2026-07-17 08:37:52 +00:00
}
2026-08-27 01:06:17 +00:00
if validationErr != nil {
return report , candidate . terminal . record ( payload , validationErr )
}
return report , candidate . terminal . record ( payload , nil )
2026-07-17 08:37:52 +00:00
} )
result . err = err
2026-08-27 01:27:25 +00:00
summary := validationSummary ( terminalResult , StageExtract , input . stepID , lane . ID , lane . Extract . Module , chunk . ID , chunk . Index )
if debugErr := writeProducerTerminalDebug ( input . Debug , path . Join ( "extract" , fileio . EncodePathComponent ( lane . ID ) , fmt . Sprintf ( "chunk-%06d" , chunk . Index + 1 ) , "terminal.json" ) , terminalResult , lane . ExtractValidationPolicy , summary ) ; debugErr != nil {
result . err = errors . Join ( result . err , debugErr )
return result
}
2026-08-27 15:29:10 +00:00
if err == nil {
diagnostics , diagnosticErr := terminalDiagnosticGroups ( terminalResult , contracts . DiagnosticOrigin { Stage : contracts . DiagnosticOriginStageExtract , StepID : input . stepID , LaneID : lane . ID , ModuleKey : lane . Extract . Module } , & chunk )
if diagnosticErr != nil {
result . err = fmt . Errorf ( "promote extract diagnostics: %w" , diagnosticErr )
return result
}
result . diagnostics = diagnostics
}
2026-08-27 01:06:17 +00:00
if err == nil && terminalResult . Action == producerTerminalRejected {
result . rejected = terminalResult . Rejection
if result . rejected != nil {
result . rejected . Stage , result . rejected . StepID , result . rejected . LaneID , result . rejected . ModuleKey , result . rejected . ChunkID , result . rejected . ChunkIndex = string ( StageExtract ) , input . stepID , lane . ID , lane . Extract . Module , chunk . ID , chunk . Index
2026-08-27 01:27:25 +00:00
result . validationSummary = & summary
result . rejected . Validation = cloneValidationSummaryPtr ( result . validationSummary )
2026-08-27 01:06:17 +00:00
}
2026-07-17 08:37:52 +00:00
return result
}
2026-08-27 01:06:17 +00:00
if err == nil {
candidate , ok := terminalResult . Value . ( extractAttemptValue )
if ! ok {
result . err = fmt . Errorf ( "extract attempt terminal has incompatible value" )
return result
}
stored , encodeErr := checkpointArtifact ( typed . codec , candidate . artifact . LaneID , candidate . artifact . ExtractorKey , candidate . artifact . SourceID , candidate . artifact . Value )
2026-08-27 16:40:17 +00:00
payload := map [ string ] any { "output" : debugCheckpointArtifact ( candidate . serialized ) , "rejection" : debugRejectedOutputPtr ( nil ) }
2026-08-27 01:06:17 +00:00
if encodeErr != nil {
attemptErr := fmt . Errorf ( "serialize accepted extract output for lane %q chunk %q: %w" , lane . ID , chunk . ID , encodeErr )
result . err = candidate . terminal . record ( payload , attemptErr )
return result
}
stored . ChunkID , stored . ChunkIndex , stored . ChunkRef = candidate . artifact . ChunkID , candidate . artifact . ChunkIndex , candidate . artifact . ChunkRef
2026-08-27 15:40:45 +00:00
stored . Diagnostics = terminalCheckpointDiagnostics ( terminalResult )
2026-08-27 01:06:17 +00:00
if debugErr := candidate . terminal . record ( payload , nil ) ; debugErr != nil {
result . err = debugErr
return result
}
result . value , result . serialized = candidate . artifact , stored
result . validationIncomplete = terminalResult . ValidationIncomplete
2026-08-27 01:27:25 +00:00
result . validationSummary = & summary
2026-08-27 01:06:17 +00:00
}
2026-07-17 08:37:52 +00:00
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 ]
2026-08-27 01:27:25 +00:00
if result . validationSummary != nil {
state . validationSummaries = append ( state . validationSummaries , artifacts . CloneValidationSummary ( * result . validationSummary ) )
}
2026-07-17 08:37:52 +00:00
if result . rejected != nil {
state . rejected = append ( state . rejected , * result . rejected )
2026-08-27 15:29:10 +00:00
state . diagnostics = append ( state . diagnostics , result . diagnostics ... )
2026-07-17 08:37:52 +00:00
continue
}
state . values = append ( state . values , result . value )
state . serialized = append ( state . serialized , result . serialized )
2026-08-27 15:29:10 +00:00
state . diagnostics = append ( state . diagnostics , result . diagnostics ... )
2026-08-27 01:06:17 +00:00
if result . validationIncomplete {
state . incomplete = append ( state . incomplete , result . chunkIndex )
}
2026-07-17 08:37:52 +00:00
}
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 } )
2026-08-27 01:06:17 +00:00
sort . Ints ( state . incomplete )
2026-08-27 11:50:37 +00:00
if len ( state . incomplete ) > 0 {
state . reuseEligible = false
}
if ! state . decision . Reused && state . reuseEligible {
2026-08-27 16:40:17 +00:00
if err := recordExtract ( checkpoints , stepID , lane . ID , lane . Extract . Module , state . deps , state . serialized , state . rejected ) ; 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 {
2026-08-27 01:27:25 +00:00
accepted : state . values ,
serialized : state . serialized ,
2026-08-27 15:29:10 +00:00
diagnostics : state . diagnostics ,
2026-08-27 01:27:25 +00:00
rejected : state . rejected ,
incomplete : state . incomplete ,
validationSummaries : state . validationSummaries ,
decision : state . decision ,
2026-08-27 11:50:37 +00:00
reuseEligible : state . reuseEligible ,
2026-07-17 14:05:21 +00:00
}
2026-08-27 15:29:10 +00:00
local . diagnosticGroups = append ( local . diagnosticGroups , contracts . CloneDiagnosticCollection ( contracts . DiagnosticCollection { Groups : results . diagnostics } ) . Groups ... )
2026-07-17 14:05:21 +00:00
local . Rejected = append ( local . Rejected , cloneRejectedOutputs ( results . rejected ) ... )
2026-08-27 01:27:25 +00:00
local . ValidationSummaries = append ( local . ValidationSummaries , cloneValidationSummaries ( results . validationSummaries ) ... )
2026-08-09 00:41:28 +00:00
if err := writeDebugTimed ( input . Debug , path . Join ( "extract" , fileio . EncodePathComponent ( 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-08-27 16:40:17 +00:00
if err := writeDebugTimed ( input . Debug , path . Join ( "extract" , fileio . EncodePathComponent ( 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 ) , "validation_incomplete_chunks" : append ( [ ] int ( nil ) , results . incomplete ... ) } } ) ; 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 ) ... )
2026-08-27 15:29:10 +00:00
appendDiagnosticGroups ( dst , src . diagnosticGroups )
2026-07-17 08:37:52 +00:00
dst . CheckpointEvents = append ( dst . CheckpointEvents , src . CheckpointEvents ... )
2026-08-27 01:27:25 +00:00
dst . ValidationSummaries = append ( dst . ValidationSummaries , cloneValidationSummaries ( src . ValidationSummaries ) ... )
2026-08-27 11:50:37 +00:00
if len ( src . normalizeReuseEligibility ) > 0 {
if dst . normalizeReuseEligibility == nil {
dst . normalizeReuseEligibility = make ( map [ generatedOutputKey ] bool , len ( src . normalizeReuseEligibility ) )
}
for key , eligible := range src . normalizeReuseEligibility {
dst . normalizeReuseEligibility [ key ] = eligible
}
}
2026-07-17 08:37:52 +00:00
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
}