2026-07-03 10:01:28 -05:00
package pipeline
2026-07-03 12:50:09 +00:00
import (
"context"
2026-07-05 17:51:36 +00:00
"crypto/sha256"
"encoding/hex"
2026-07-17 07:19:58 +00:00
"encoding/json"
2026-07-17 15:23:27 +00:00
"errors"
2026-07-03 12:50:09 +00:00
"fmt"
2026-07-05 17:51:36 +00:00
"mime"
2026-07-04 00:45:21 +00:00
"path"
2026-07-05 17:51:36 +00:00
"path/filepath"
2026-07-05 18:21:37 +00:00
"sort"
2026-07-04 00:45:21 +00:00
"strings"
"time"
2026-07-03 12:50:09 +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-03 12:50:09 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
2026-07-23 14:58:14 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/framework/chunkmap"
2026-07-03 12:50:09 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
2026-07-03 15:46:42 +00:00
type Registries struct {
2026-07-27 18:04:12 +00:00
Inputs * InputAdapterRegistry
Chunkers * ChunkerRegistry
ArtifactCodecs * ArtifactCodecRegistry
ArtifactEvidence * ArtifactEvidenceRegistry
Extractors * ExtractorRegistry
Mergers * MergerRegistry
Normalizers * NormalizerRegistry
Validators * ValidatorRegistry
ValidatorChains * ValidatorChainRegistry
Outputs * OutputEncoderRegistry
2026-07-03 12:50:09 +00:00
}
2026-07-17 06:18:46 +00:00
type Runner struct { }
2026-07-03 12:50:09 +00:00
2026-07-17 06:18:46 +00:00
func New ( ) * Runner {
return & Runner { }
2026-07-03 12:50:09 +00:00
}
type RunInput struct {
2026-07-21 21:59:48 +00:00
Prepared * PreparedPipeline
SourceID string
Path string
RawInput [ ] byte
SessionID string
RunID string
StartedAt time . Time
LLMProfiles [ ] artifacts . LLMProfileManifest
Metadata map [ string ] any
2026-08-27 15:29:10 +00:00
Diagnostics [ ] contracts . ProducerDiagnostic
2026-07-21 21:59:48 +00:00
ChunkCacheMode ChunkCacheMode
ChunkPlans ChunkPlanStore
Checkpoints CheckpointRecorder
Checkpoint CheckpointLoader
CheckpointPolicy CheckpointExecutionPolicy
Debug DebugRecorder
2026-07-17 08:37:52 +00:00
// ExtractWorkers bounds run-wide extract jobs. Values less than one use a
// single worker so direct framework callers retain deterministic behavior.
ExtractWorkers int
2026-07-17 06:18:46 +00:00
2026-08-27 11:50:37 +00:00
pipeline ResolvedPipeline
llmClient contracts . StructuredLLMClient
stepID string
references map [ referenceTargetKey ] contracts . ReferenceSet
referenceReuseEligibility map [ referenceTargetKey ] bool
2026-07-03 12:50:09 +00:00
}
type RunOutput struct {
2026-08-27 15:29:10 +00:00
Manifest artifacts . RunManifest ` json:"manifest" `
ChunkPlan * artifacts . ChunkPlanSummary ` json:"chunk_plan,omitempty" `
NormalizeOutputs [ ] contracts . SerializedOutput ` json:"normalize_outputs,omitempty" `
Rejected [ ] contracts . RejectedOutput ` json:"rejected,omitempty" `
Diagnostics contracts . DiagnosticCollection ` json:"diagnostics,omitempty" `
OutputFiles [ ] contracts . OutputFile ` json:"-" `
CheckpointEvents [ ] CheckpointEvent ` json:"checkpoint_events,omitempty" `
ValidationSummaries [ ] artifacts . ValidationSummary ` json:"validation_summaries,omitempty" `
2026-08-27 11:50:37 +00:00
normalizeReuseEligibility map [ generatedOutputKey ] bool
2026-08-27 15:29:10 +00:00
diagnosticGroups [ ] contracts . DiagnosticGroup
2026-07-03 12:50:09 +00:00
}
2026-07-05 18:21:37 +00:00
func ( r * Runner ) Run ( ctx context . Context , input RunInput ) ( output RunOutput , err error ) {
2026-07-03 12:50:09 +00:00
if r == nil {
return output , fmt . Errorf ( "runner must not be nil" )
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
return output , err
}
2026-07-03 15:46:42 +00:00
if err := validateRunInput ( input ) ; err != nil {
return output , err
2026-07-03 12:50:09 +00:00
}
2026-07-17 23:16:16 -05:00
input . Metadata , err = cloneMetadata ( input . Metadata )
if err != nil {
return output , fmt . Errorf ( "clone run metadata: %w" , err )
}
2026-07-17 06:18:46 +00:00
input . pipeline = input . Prepared . resolved
input . llmClient = input . Prepared . dependencies . LLM
2026-07-03 15:46:42 +00:00
2026-07-04 00:45:21 +00:00
output . Manifest = manifestFromPipeline ( input )
2026-08-03 19:45:27 +00:00
sessionID := strings . TrimSpace ( input . SessionID )
output . Manifest . Metadata , err = manifestMetadataWithSessionID ( output . Manifest . Metadata , sessionID )
if err != nil {
return failOutput ( output ) , err
}
2026-07-18 00:45:01 +00:00
output . ChunkPlan = & artifacts . ChunkPlanSummary {
Mode : string ( effectiveChunkCacheMode ( input . ChunkCacheMode ) ) , RequestedModule : input . pipeline . Chunk . Module ,
LookupStatus : "skipped" , LookupReason : "chunk plan lookup skipped" ,
ValidationStatus : "not_run" , PublicationStatus : "not_requested" ,
}
2026-07-08 02:46:38 +00:00
checkpoints := input . Checkpoints
if checkpoints == nil {
checkpoints = NoopCheckpointRecorder ( )
}
2026-07-08 03:02:50 +00:00
checkpointLoader := input . Checkpoint
if checkpointLoader == nil {
checkpointLoader = NoopCheckpointLoader ( )
}
2026-07-08 03:14:59 +00:00
debugRecorder := input . Debug
if debugRecorder == nil {
debugRecorder = NoopDebugRecorder ( )
}
2026-07-17 08:37:52 +00:00
checkpoints = synchronizedCheckpointRecorder ( checkpoints )
checkpointLoader = synchronizedCheckpointLoader ( checkpointLoader )
debugRecorder = synchronizedDebugRecorder ( debugRecorder )
2026-07-08 03:14:59 +00:00
input . Debug = debugRecorder
2026-07-17 06:18:46 +00:00
input . llmClient = wrapDebugLLMClient ( input . llmClient , debugRecorder )
2026-07-05 18:21:37 +00:00
defer func ( ) {
2026-07-17 06:18:46 +00:00
output . Manifest . LLMProfiles = mergeLLMProfileManifests ( input . LLMProfiles , llmProfileManifests ( input . llmClient ) )
2026-07-05 18:21:37 +00:00
} ( )
2026-08-27 15:29:10 +00:00
inputDiagnostics , diagnosticErr := promoteProducerDiagnostics ( input . Diagnostics , contracts . DiagnosticOrigin { Stage : contracts . DiagnosticOriginStageReferences } , nil )
if diagnosticErr != nil {
return failOutput ( output ) , fmt . Errorf ( "promote input diagnostics: %w" , diagnosticErr )
}
appendDiagnosticGroups ( & output , inputDiagnostics )
2026-07-08 03:14:59 +00:00
if err := writeDebugTimed ( debugRecorder , "run.json" , debugTimedEnvelope {
Stage : "run" ,
StartedAt : startedTime ( input . StartedAt ) ,
Payload : map [ string ] any {
2026-07-17 06:18:46 +00:00
"pipeline_id" : input . pipeline . ID ,
"pipeline_digest" : input . pipeline . Digest ,
2026-07-08 03:14:59 +00:00
"run_id" : output . Manifest . RunID ,
} ,
} ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write debug run artifact: %w" , err )
}
2026-07-03 15:46:42 +00:00
2026-07-17 06:18:46 +00:00
adapter := input . Prepared . input
2026-07-17 23:16:16 -05:00
if err := attachModuleManifestMetadata ( & output , "input" , adapter ) ; err != nil {
return failOutput ( output ) , err
}
2026-07-08 03:02:50 +00:00
sourceCheckpoint , sourceDecision := checkpointLoader . Source ( adapter . Key ( ) )
2026-07-21 21:59:48 +00:00
recordCheckpointEvent ( & output , checkpointLoader , "source" , "" , "" , adapter . Key ( ) , sourceDecision )
2026-07-08 03:02:50 +00:00
doc := sourceCheckpoint . Document
2026-07-08 03:14:59 +00:00
sourceStarted := time . Now ( ) . UTC ( )
if err := writeDebugTimed ( debugRecorder , "source/input.json" , debugTimedEnvelope {
Stage : "source" ,
ModuleKey : adapter . Key ( ) ,
StartedAt : sourceStarted ,
Payload : debugSourceInput {
SourceID : input . SourceID ,
Path : input . Path ,
Raw : debugContentEnvelope ( input . RawInput , sourceInputMediaType ( input . Path ) , nil , nil ) ,
2026-07-17 06:18:46 +00:00
Options : redactSensitiveMap ( input . pipeline . Input . Options ) ,
2026-07-08 03:14:59 +00:00
Metadata : redactSensitiveMap ( input . Metadata ) ,
} ,
} ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write source debug artifact: %w" , err )
}
2026-07-08 03:02:50 +00:00
if ! sourceDecision . Reused {
if err := checkpoints . SourceRunning ( adapter . Key ( ) ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write source checkpoint: %w" , err )
}
2026-07-17 23:16:16 -05:00
requestMetadata , metadataErr := cloneMetadata ( input . Metadata )
if metadataErr != nil {
return failOutput ( output ) , fmt . Errorf ( "clone input adapter metadata: %w" , metadataErr )
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
_ = checkpoints . SourceFailed ( adapter . Key ( ) , err )
return failOutput ( output ) , err
}
2026-07-08 03:02:50 +00:00
doc , err = adapter . Parse ( ctx , contracts . ParseRequest {
2026-08-25 19:39:04 +00:00
SourceID : input . SourceID ,
Path : input . Path ,
Raw : input . RawInput ,
LLMProfile : input . pipeline . Input . LLMProfile ,
2026-08-25 19:55:39 +00:00
StructuredOutputRepairAttempts : cloneStructuredOutputRepairAttempts ( input . pipeline . Input . StructuredOutputRepairAttempts ) ,
2026-08-25 19:39:04 +00:00
Metadata : requestMetadata ,
2026-07-08 03:02:50 +00:00
} )
2026-08-09 00:50:02 +00:00
if ctxErr := ctx . Err ( ) ; ctxErr != nil {
_ = checkpoints . SourceFailed ( adapter . Key ( ) , ctxErr )
return failOutput ( output ) , ctxErr
}
2026-07-08 03:02:50 +00:00
if err != nil {
_ = checkpoints . SourceFailed ( adapter . Key ( ) , err )
return failOutput ( output ) , fmt . Errorf ( "parse input with adapter %q: %w" , adapter . Key ( ) , err )
}
if err := source . ValidateDocument ( doc ) ; err != nil {
_ = checkpoints . SourceFailed ( adapter . Key ( ) , err )
return failOutput ( output ) , fmt . Errorf ( "validate source document: %w" , err )
}
if err := checkpoints . SourceSucceeded ( adapter . Key ( ) , doc ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write source checkpoint: %w" , err )
}
2026-07-08 02:46:38 +00:00
}
2026-07-08 03:14:59 +00:00
if err := writeDebugTimed ( debugRecorder , "source/output.json" , debugTimedEnvelope {
Stage : "source" ,
ModuleKey : adapter . Key ( ) ,
StartedAt : sourceStarted ,
Payload : map [ string ] any {
"reused" : sourceDecision . Reused ,
"decision" : sourceDecision ,
"document" : debugSourceDocumentEnvelope ( doc ) ,
} ,
} ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write source debug artifact: %w" , err )
}
2026-07-05 17:51:36 +00:00
sourceInput := sourceInputMaterial ( input . Path , input . RawInput )
2026-07-03 15:46:42 +00:00
output . Manifest . SourceDigests = [ ] string { doc . Digest }
2026-07-17 06:18:46 +00:00
chunker := input . Prepared . chunker
2026-07-17 23:16:16 -05:00
if err := attachModuleManifestMetadata ( & output , "chunker" , chunker ) ; err != nil {
return failOutput ( output ) , err
}
2026-07-08 03:14:59 +00:00
chunkStarted := time . Now ( ) . UTC ( )
2026-07-18 00:20:56 +00:00
chunkMode := effectiveChunkCacheMode ( input . ChunkCacheMode )
2026-07-08 03:14:59 +00:00
if err := writeDebugTimed ( debugRecorder , "chunk/input.json" , debugTimedEnvelope {
Stage : string ( StageChunk ) ,
ModuleKey : chunker . Key ( ) ,
StartedAt : chunkStarted ,
Payload : map [ string ] any {
2026-07-18 00:20:56 +00:00
"cache_mode" : chunkMode ,
2026-07-08 03:14:59 +00:00
"source" : debugSourceDocumentEnvelope ( doc ) ,
"source_input" : debugContentEnvelope ( sourceInput . Content , sourceInput . MediaType , nil , nil ) ,
2026-07-17 06:18:46 +00:00
"options" : redactSensitiveMap ( input . pipeline . Chunk . Options ) ,
2026-07-08 03:14:59 +00:00
"metadata" : redactSensitiveMap ( input . Metadata ) ,
} ,
} ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write chunk debug artifact: %w" , err )
}
2026-07-18 00:20:56 +00:00
chunkResult , err := r . runChunkPlan ( ctx , input , doc , sourceInput , sessionID )
2026-07-17 23:16:16 -05:00
if applyErr := applyChunkPlanExecution ( & output , chunkResult ) ; applyErr != nil {
return failOutput ( output ) , applyErr
}
2026-07-18 00:20:56 +00:00
if err != nil {
return failOutput ( output ) , err
}
if chunkResult . rejection != nil {
output . Rejected = append ( output . Rejected , * chunkResult . rejection )
}
2026-08-27 01:27:25 +00:00
if chunkResult . validation != nil {
output . ValidationSummaries = append ( output . ValidationSummaries , artifacts . CloneValidationSummary ( * chunkResult . validation ) )
}
2026-08-27 15:29:10 +00:00
appendDiagnosticGroups ( & output , chunkResult . diagnostics )
2026-07-08 03:14:59 +00:00
chunkDebugPayload := map [ string ] any {
2026-07-18 00:20:56 +00:00
"cache_mode" : chunkMode ,
"lookup" : chunkResult . lookup ,
"accepted" : chunkResult . accepted ,
"materialized_chunks" : debugSourceChunkEnvelopes ( chunkResult . chunks ) ,
2026-07-17 23:57:59 +00:00
}
2026-07-18 00:20:56 +00:00
if chunkResult . plan != nil {
chunkDebugPayload [ "plan" ] = debugChunkPlanEnvelope ( * chunkResult . plan )
2026-07-08 03:14:59 +00:00
}
2026-07-18 00:20:56 +00:00
if chunkResult . rejection != nil {
chunkDebugPayload [ "rejection" ] = debugRejectedOutputEnvelope ( * chunkResult . rejection )
2026-07-08 03:14:59 +00:00
}
if err := writeDebugTimed ( debugRecorder , "chunk/output.json" , debugTimedEnvelope {
Stage : string ( StageChunk ) ,
ModuleKey : chunker . Key ( ) ,
StartedAt : chunkStarted ,
Payload : chunkDebugPayload ,
} ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write chunk debug artifact: %w" , err )
}
2026-07-03 12:50:09 +00:00
2026-07-23 14:58:14 +00:00
var acceptedChunkMap * contracts . SerializedArtifact
if chunkResult . accepted {
if chunkResult . plan == nil || chunkResult . record == nil {
return failOutput ( output ) , fmt . Errorf ( "accepted chunk plan is missing plan or producer record" )
}
artifact , buildErr := chunkmap . Serialize ( chunkmap . BuildRequest {
Source : doc ,
Plan : * chunkResult . plan ,
Chunks : chunkResult . chunks ,
RequestedChunker : input . pipeline . Chunk . Module ,
Producer : chunkmap . Producer {
InputModule : chunkResult . record . Producer . InputModule ,
ChunkModule : chunkResult . record . Producer . ChunkModule ,
LLMProfile : chunkResult . record . Producer . LLMProfile ,
} ,
} )
if buildErr != nil {
return failOutput ( output ) , fmt . Errorf ( "serialize accepted chunk map: %w" , buildErr )
}
acceptedChunkMap = & artifact
}
2026-07-18 00:20:56 +00:00
if chunkResult . accepted {
2026-07-22 02:03:27 +00:00
if err := r . runPreparedSteps ( ctx , input , checkpoints , checkpointLoader , doc , sourceInput , sessionID , chunkResult . chunks , & output ) ; err != nil {
return failOutput ( output ) , err
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
return failOutput ( output ) , err
}
2026-07-03 15:46:42 +00:00
if len ( output . Rejected ) > 0 {
output . Manifest . ValidationStatus = "rejected"
2026-08-27 01:27:25 +00:00
} else if hasIncompleteValidation ( output . ValidationSummaries ) {
output . Manifest . ValidationStatus = "incomplete"
2026-07-03 15:46:42 +00:00
} else {
output . Manifest . ValidationStatus = "approved"
}
2026-07-17 08:13:08 +00:00
populateOutputManifest ( & output )
2026-07-04 00:45:21 +00:00
output . Manifest . CompletedAt = timePtr ( time . Now ( ) . UTC ( ) )
2026-08-27 15:29:10 +00:00
if err := finalizeDiagnostics ( & output ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "aggregate diagnostics: %w" , err )
}
2026-07-03 15:46:42 +00:00
2026-07-17 06:18:46 +00:00
encoder := input . Prepared . output
2026-07-17 23:16:16 -05:00
if err := attachModuleManifestMetadata ( & output , "output" , encoder ) ; err != nil {
return failOutput ( output ) , err
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
return failOutput ( output ) , err
}
2026-07-08 03:14:59 +00:00
outputStarted := time . Now ( ) . UTC ( )
2026-07-27 18:17:54 +00:00
evidenceArtifact , evidenceSummary , err := buildOutputEvidenceContext ( input . Prepared , doc , output . NormalizeOutputs )
if err != nil {
return failOutput ( output ) , err
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
return failOutput ( output ) , err
}
2026-07-27 18:17:54 +00:00
if evidenceSummary != nil {
if err := writeDebugTimed ( debugRecorder , "output/evidence-context.json" , debugTimedEnvelope {
Stage : string ( StageOutput ) ,
ModuleKey : encoder . Key ( ) ,
StartedAt : outputStarted ,
Payload : * evidenceSummary ,
} ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write evidence context debug artifact: %w" , err )
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
return failOutput ( output ) , err
}
2026-07-27 18:17:54 +00:00
}
2026-07-23 14:58:14 +00:00
outputDebugPayload := map [ string ] any {
"manifest" : output . Manifest ,
"normalize_outputs" : debugSerializedOutputEnvelopes ( output . NormalizeOutputs ) ,
"rejected" : debugRejectedOutputEnvelopes ( output . Rejected ) ,
"options" : redactSensitiveMap ( input . pipeline . Output . Options ) ,
"metadata" : redactSensitiveMap ( input . Metadata ) ,
}
if acceptedChunkMap != nil {
outputDebugPayload [ "chunk_map" ] = debugSerializedOutputEnvelope ( contracts . SerializedOutput { Artifact : * acceptedChunkMap } )
}
2026-07-08 03:14:59 +00:00
if err := writeDebugTimed ( debugRecorder , "output/input.json" , debugTimedEnvelope {
Stage : string ( StageOutput ) ,
ModuleKey : encoder . Key ( ) ,
StartedAt : outputStarted ,
2026-07-23 14:58:14 +00:00
Payload : outputDebugPayload ,
2026-07-08 03:14:59 +00:00
} ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write output debug artifact: %w" , err )
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
return failOutput ( output ) , err
}
2026-07-17 23:16:16 -05:00
outputMetadata , err := cloneMetadata ( input . Metadata )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "clone output encoder metadata: %w" , err )
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
return failOutput ( output ) , err
}
2026-07-03 15:46:42 +00:00
encoded , err := encoder . Encode ( ctx , contracts . OutputRequest {
2026-08-25 19:39:04 +00:00
Manifest : output . Manifest ,
NormalizeOutputs : cloneSerializedOutputs ( output . NormalizeOutputs ) ,
Rejected : cloneRejectedOutputs ( output . Rejected ) ,
2026-08-27 15:29:10 +00:00
Diagnostics : contracts . CloneDiagnosticCollection ( output . Diagnostics ) ,
2026-08-25 19:39:04 +00:00
LLMProfile : input . pipeline . Output . LLMProfile ,
2026-08-25 19:55:39 +00:00
StructuredOutputRepairAttempts : cloneStructuredOutputRepairAttempts ( input . pipeline . Output . StructuredOutputRepairAttempts ) ,
2026-08-25 19:39:04 +00:00
Metadata : outputMetadata ,
ChunkMap : contracts . CloneSerializedArtifactPointer ( acceptedChunkMap ) ,
EvidenceContext : contracts . CloneSerializedArtifactPointer ( evidenceArtifact ) ,
2026-07-03 15:46:42 +00:00
} )
2026-08-09 00:50:02 +00:00
if ctxErr := ctx . Err ( ) ; ctxErr != nil {
return failOutput ( output ) , ctxErr
}
2026-07-03 15:46:42 +00:00
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "encode output with encoder %q: %w" , encoder . Key ( ) , err )
}
2026-07-04 00:45:21 +00:00
files , err := outputFilesFromResult ( encoded )
if err != nil {
return failOutput ( output ) , fmt . Errorf ( "validate output files from encoder %q: %w" , encoder . Key ( ) , err )
}
2026-07-08 03:14:59 +00:00
if err := writeDebugTimed ( debugRecorder , "output/output.json" , debugTimedEnvelope {
Stage : string ( StageOutput ) ,
ModuleKey : encoder . Key ( ) ,
StartedAt : outputStarted ,
Payload : map [ string ] any {
2026-08-27 15:33:13 +00:00
"files" : debugOutputFiles ( files ) ,
2026-07-08 03:14:59 +00:00
} ,
} ) ; err != nil {
return failOutput ( output ) , fmt . Errorf ( "write output debug artifact: %w" , err )
}
2026-08-09 00:50:02 +00:00
if err := ctx . Err ( ) ; err != nil {
return failOutput ( output ) , err
}
output . OutputFiles = files
2026-07-03 15:46:42 +00:00
return output , nil
}
2026-08-27 01:27:25 +00:00
func hasIncompleteValidation ( summaries [ ] artifacts . ValidationSummary ) bool {
for _ , summary := range summaries {
if summary . Status == "incomplete" {
return true
}
}
return false
}
2026-07-22 02:03:27 +00:00
func ( r * Runner ) runPreparedSteps ( ctx context . Context , input RunInput , checkpoints CheckpointRecorder , loader CheckpointLoader , doc * source . SourceDocument , sourceInput contracts . LLMInputMaterial , sessionID string , chunks [ ] source . Chunk , output * RunOutput ) error {
for _ , step := range input . Prepared . Steps {
stepInput := input
stepInput . stepID = step . ID
stepReferences , referenceProvenance , err := buildStepReferenceSets ( input , step , output . NormalizeOutputs )
if err != nil {
return fmt . Errorf ( "prepare generated references for pipeline step %q: %w" , step . ID , err )
}
stepInput . references = stepReferences
2026-08-27 11:50:37 +00:00
stepInput . referenceReuseEligibility = buildStepReferenceReuseEligibility ( step , output . normalizeReuseEligibility )
2026-07-22 02:03:27 +00:00
output . Manifest . References = append ( output . Manifest . References , referenceProvenance ... )
laneOutput , laneErr := r . runLanes ( ctx , stepInput , step , checkpoints , loader , doc , sourceInput , sessionID , chunks )
if err := mergeLaneOutput ( output , laneOutput ) ; err != nil {
return err
}
if laneErr != nil {
return fmt . Errorf ( "execute pipeline step %q: %w" , step . ID , laneErr )
}
}
return nil
}
2026-08-09 01:19:55 +00:00
type retryAttemptResult struct {
accepted bool
rejection * contracts . RejectedOutput
}
2026-08-27 00:27:27 +00:00
func runSimpleRetry ( ctx context . Context , retries int , run func ( attempt int ) ( retryAttemptResult , error ) ) ( retryAttemptResult , error ) {
2026-07-17 08:13:08 +00:00
attempts := retries + 1
2026-08-09 01:19:55 +00:00
var last retryAttemptResult
2026-07-07 19:19:04 +00:00
for attempt := 1 ; attempt <= attempts ; attempt ++ {
if err := ctx . Err ( ) ; err != nil {
2026-08-09 01:19:55 +00:00
return retryAttemptResult { } , err
2026-07-07 19:19:04 +00:00
}
2026-08-09 01:19:55 +00:00
result , err := run ( attempt )
2026-07-07 19:19:04 +00:00
if err != nil {
2026-07-17 11:29:52 -05:00
var debugErr * attemptDebugPersistenceError
if errors . As ( err , & debugErr ) {
2026-08-09 01:19:55 +00:00
return retryAttemptResult { } , fmt . Errorf ( "failed after %d attempt(s): %w" , attempt , err )
2026-07-17 11:29:52 -05:00
}
2026-07-07 19:19:04 +00:00
if attempt == attempts {
2026-08-09 01:19:55 +00:00
return retryAttemptResult { } , fmt . Errorf ( "failed after %d attempt(s): %w" , attempt , err )
2026-07-07 19:19:04 +00:00
}
2026-07-17 08:37:52 +00:00
if ctxErr := ctx . Err ( ) ; ctxErr != nil {
2026-08-09 01:19:55 +00:00
return retryAttemptResult { } , ctxErr
2026-07-17 08:37:52 +00:00
}
2026-07-07 19:19:04 +00:00
continue
}
2026-08-09 01:19:55 +00:00
if result . accepted {
return result , nil
2026-07-07 19:19:04 +00:00
}
2026-08-09 01:19:55 +00:00
if result . rejection != nil {
rejection := * result . rejection
2026-07-07 19:19:04 +00:00
rejection . AttemptCount = attempt
2026-08-27 16:40:17 +00:00
last = retryAttemptResult { rejection : & rejection }
2026-07-07 19:19:04 +00:00
}
if ctxErr := ctx . Err ( ) ; ctxErr != nil {
2026-08-09 01:19:55 +00:00
return retryAttemptResult { } , ctxErr
2026-07-07 19:19:04 +00:00
}
if attempt == attempts {
2026-08-09 01:19:55 +00:00
if last . rejection == nil {
last . rejection = & contracts . RejectedOutput { ReasonCode : "output_rejected" , Message : "output rejected" , AttemptCount : attempt }
2026-07-07 19:19:04 +00:00
}
2026-08-09 01:19:55 +00:00
return last , nil
2026-07-07 19:19:04 +00:00
}
}
2026-08-09 01:19:55 +00:00
return last , nil
2026-07-07 19:19:04 +00:00
}
2026-08-27 00:35:37 +00:00
func ( r * Runner ) validateChunkReport ( ctx context . Context , doc * source . SourceDocument , moduleKey string , chunks [ ] source . Chunk , sourceInput contracts . LLMInputMaterial , sessionID string , references contracts . ReferenceSet , metadata map [ string ] any , prepared preparedValidatorChain , attempt int , debug DebugRecorder ) ( validationReport , error ) {
2026-07-17 07:19:58 +00:00
content , err := json . Marshal ( chunks )
if err != nil {
2026-08-27 00:35:37 +00:00
return validationReport { } , fmt . Errorf ( "encode canonical chunks for validation: %w" , err )
2026-07-17 07:19:58 +00:00
}
schema := contracts . ArtifactSchema { ID : "notarius.source.chunks" , Name : "notarius_source_chunks" , Version : "v1" , JSONSchema : [ ] byte ( ` { "$schema":"https://json-schema.org/draft/2020-12/schema","type":"array"} ` ) }
2026-08-27 00:18:16 +00:00
report , err := executeValidationChain ( ctx , prepared , func ( validatorCtx context . Context , item preparedValidator , validatorAttempt int ) ( validationInvocation , error ) {
2026-07-17 07:19:58 +00:00
binding := item . resolved . Binding
started := time . Now ( ) . UTC ( )
2026-08-27 00:18:16 +00:00
attemptPath := validatorAttemptPath ( path . Join ( "validate" , fileio . EncodePathComponent ( string ( StageChunk ) ) , "" , fileio . EncodePathComponent ( moduleKey ) , fmt . Sprintf ( "%02d-%s-attempt-%02d" , item . position , fileio . EncodePathComponent ( binding . Module ) , attempt ) ) , validatorAttempt )
validatorCtx , llmScope := withIsolatedDebugLLMScope ( validatorCtx , attemptPath )
2026-07-17 07:19:58 +00:00
var result contracts . ValidationResult
2026-07-17 23:16:16 -05:00
requestMetadata , cloneErr := cloneMetadata ( metadata )
if cloneErr != nil {
2026-08-27 00:18:16 +00:00
return validationInvocation { } , fatalValidationError ( fmt . Errorf ( "clone chunk validation metadata: %w" , cloneErr ) )
2026-07-17 23:16:16 -05:00
}
requestChunks , cloneErr := cloneSourceChunks ( chunks )
if cloneErr != nil {
2026-08-27 00:18:16 +00:00
return validationInvocation { } , fatalValidationError ( fmt . Errorf ( "clone chunks for validation: %w" , cloneErr ) )
2026-07-17 23:16:16 -05:00
}
2026-07-17 07:19:58 +00:00
switch item . resolved . Target {
case ValidatorTargetChunk :
2026-08-25 23:46:52 +00:00
result , err = item . chunk . Validate ( validatorCtx , contracts . ChunkValidationRequest { ModuleKey : moduleKey , Source : doc , SourceID : doc . ID , SourceInput : sourceInput . Clone ( ) , SessionID : sessionID , References : CloneReferenceSet ( references ) , LLMProfile : binding . LLMProfile , StructuredOutputRepairAttempts : cloneStructuredOutputRepairAttempts ( binding . StructuredOutputRepairAttempts ) , Metadata : requestMetadata , Chunks : requestChunks } )
2026-07-17 07:19:58 +00:00
case ValidatorTargetSerialized :
2026-08-25 23:46:52 +00:00
result , err = item . serialized . Validate ( validatorCtx , contracts . SerializedValidationRequest { Stage : string ( StageChunk ) , ModuleKey : moduleKey , Source : doc , SourceID : doc . ID , SourceInput : sourceInput . Clone ( ) , SessionID : sessionID , References : CloneReferenceSet ( references ) , LLMProfile : binding . LLMProfile , StructuredOutputRepairAttempts : cloneStructuredOutputRepairAttempts ( binding . StructuredOutputRepairAttempts ) , Metadata : requestMetadata , Chunks : requestChunks , Schema : contracts . CloneArtifactSchema ( schema ) , MediaType : "application/json" , Content : append ( [ ] byte ( nil ) , content ... ) } )
2026-07-17 07:19:58 +00:00
default :
2026-08-27 00:18:16 +00:00
return validationInvocation { } , fatalValidationError ( fmt . Errorf ( "validator %q is incompatible with chunk validation" , binding . Module ) )
2026-07-17 07:19:58 +00:00
}
2026-08-26 23:24:20 +00:00
if err == nil {
err = contracts . ValidateValidationResult ( result )
}
2026-07-17 08:13:08 +00:00
debugContent := debugContentEnvelope ( content , "application/json" , nil , nil )
debugContent . ContentDigest = debugContentDigest ( content )
debugCall := debugValidationCall { ValidatorName : binding . Module , Request : map [ string ] any { "stage" : string ( StageChunk ) , "module_key" : moduleKey , "source_id" : doc . ID , "schema" : schema , "schema_digest" : contracts . DigestArtifactSchema ( schema ) , "content" : debugContent , "metadata" : redactSensitiveMap ( metadata ) } , Result : debugValidationResultEnvelope ( result ) }
2026-07-17 07:19:58 +00:00
if err != nil {
debugCall . Error = err . Error ( )
}
if err != nil {
2026-07-17 15:23:27 +00:00
validationErr := fmt . Errorf ( "validate chunks with validator %q: %w" , binding . Module , err )
if debugErr := writeDebugAttempt ( debug , attemptPath , debugTimedEnvelope { Stage : string ( StageChunk ) , ModuleKey : moduleKey , Attempt : attempt , StartedAt : started , Payload : debugCall , Error : debugCall . Error } , llmScope ) ; debugErr != nil {
2026-08-27 00:18:16 +00:00
return validationInvocation { } , fatalValidationError ( errors . Join ( validationErr , fmt . Errorf ( "write chunk validator attempt debug artifact: %w" , debugErr ) ) )
2026-07-17 15:23:27 +00:00
}
2026-08-27 00:18:16 +00:00
return validationInvocation { } , validationErr
2026-07-17 15:23:27 +00:00
}
if debugErr := writeDebugAttempt ( debug , attemptPath , debugTimedEnvelope { Stage : string ( StageChunk ) , ModuleKey : moduleKey , Attempt : attempt , StartedAt : started , Payload : debugCall } , llmScope ) ; debugErr != nil {
2026-08-27 00:18:16 +00:00
return validationInvocation { } , fatalValidationError ( fmt . Errorf ( "write chunk validator attempt debug artifact: %w" , debugErr ) )
2026-07-17 07:19:58 +00:00
}
2026-08-27 00:18:16 +00:00
return validationInvocation { result : result } , nil
} )
if err != nil {
2026-08-27 00:35:37 +00:00
return report , err
2026-08-27 00:18:16 +00:00
}
2026-08-27 00:35:37 +00:00
return report , nil
2026-07-17 07:19:58 +00:00
}
2026-07-07 21:10:29 +00:00
func resolvedValidatorChain ( stage ModuleStage , laneID string , moduleKey string , chains [ ] ResolvedValidatorChain ) ResolvedValidatorChain {
for _ , chain := range chains {
if chain . Stage != stage {
continue
}
if chain . ModuleKey != moduleKey {
continue
}
if strings . TrimSpace ( chain . LaneID ) != strings . TrimSpace ( laneID ) {
continue
}
return ResolvedValidatorChain {
Stage : chain . Stage ,
LaneID : chain . LaneID ,
ModuleKey : chain . ModuleKey ,
Validators : cloneResolvedValidators ( chain . Validators ) ,
}
}
return ResolvedValidatorChain {
Stage : stage ,
LaneID : strings . TrimSpace ( laneID ) ,
ModuleKey : strings . TrimSpace ( moduleKey ) ,
}
}
2026-07-17 06:18:46 +00:00
func validateRunInput ( input RunInput ) error {
if input . Prepared == nil {
return fmt . Errorf ( "prepared pipeline must not be nil" )
2026-07-03 15:46:42 +00:00
}
2026-07-18 00:20:56 +00:00
mode := effectiveChunkCacheMode ( input . ChunkCacheMode )
if err := mode . Validate ( ) ; err != nil {
return err
}
if mode != ChunkCacheBypass && input . ChunkPlans == nil {
return fmt . Errorf ( "chunk plan store is required for %q mode" , mode )
}
2026-07-21 21:19:00 +00:00
if err := validateResolvedPipeline ( input . Prepared . resolved ) ; err != nil {
return err
}
2026-07-21 21:39:23 +00:00
return nil
2026-07-03 15:46:42 +00:00
}
2026-07-17 07:19:58 +00:00
func validateResolvedPipeline ( pipeline ResolvedPipeline ) error {
2026-07-17 06:18:46 +00:00
if pipeline . ID == "" {
2026-07-03 15:46:42 +00:00
return fmt . Errorf ( "resolved pipeline id must not be empty" )
}
2026-07-17 06:18:46 +00:00
if pipeline . Digest == "" {
2026-07-03 15:46:42 +00:00
return fmt . Errorf ( "resolved pipeline digest must not be empty" )
}
2026-07-17 06:18:46 +00:00
if pipeline . Input . Module == "" {
2026-07-03 15:46:42 +00:00
return fmt . Errorf ( "resolved pipeline input module must not be empty" )
}
2026-07-17 06:18:46 +00:00
if pipeline . Chunk . Module == "" {
2026-07-03 15:46:42 +00:00
return fmt . Errorf ( "resolved pipeline chunk module must not be empty" )
}
2026-07-17 06:18:46 +00:00
if pipeline . Output . Module == "" {
2026-07-03 15:46:42 +00:00
return fmt . Errorf ( "resolved pipeline output module must not be empty" )
}
2026-07-21 21:05:28 +00:00
if len ( pipeline . Steps ) == 0 {
2026-07-03 15:46:42 +00:00
return fmt . Errorf ( "resolved pipeline artifact lanes must not be empty" )
}
2026-07-21 21:05:28 +00:00
for _ , lane := range pipeline . AllArtifactLanes ( ) {
2026-07-03 15:46:42 +00:00
if lane . ID == "" {
return fmt . Errorf ( "resolved pipeline artifact lane id must not be empty" )
}
if lane . Extract . Module == "" {
return fmt . Errorf ( "resolved pipeline lane %q extract module must not be empty" , lane . ID )
}
if lane . Merge . Module == "" {
return fmt . Errorf ( "resolved pipeline lane %q merge module must not be empty" , lane . ID )
}
if lane . Normalize . Module == "" {
return fmt . Errorf ( "resolved pipeline lane %q normalize module must not be empty" , lane . ID )
}
2026-07-07 15:32:35 -05:00
if len ( lane . Validators ) > 0 {
2026-07-07 21:21:12 +00:00
return fmt . Errorf ( "resolved pipeline lane %q validators are not supported at artifact lane level; use extract.validators, merge.validators, or normalize.validators" , lane . ID )
2026-07-03 15:46:42 +00:00
}
}
return nil
}
2026-07-08 03:14:59 +00:00
func startedTime ( t time . Time ) time . Time {
if t . IsZero ( ) {
return time . Now ( ) . UTC ( )
}
return t . UTC ( )
}
2026-07-04 00:45:21 +00:00
func manifestFromPipeline ( input RunInput ) artifacts . RunManifest {
startedAt := input . StartedAt
if startedAt . IsZero ( ) {
startedAt = time . Now ( ) . UTC ( )
}
runID := strings . TrimSpace ( input . RunID )
if runID == "" {
runID = fmt . Sprintf ( "run-%d" , startedAt . UnixNano ( ) )
}
2026-07-17 06:18:46 +00:00
pipeline := input . pipeline
2026-07-03 15:46:42 +00:00
manifest := artifacts . RunManifest {
2026-07-18 00:45:01 +00:00
PipelineID : pipeline . ID ,
PipelineDigest : pipeline . Digest ,
InputModule : pipeline . Input . Module ,
Chunker : pipeline . Chunk . Module ,
ChunkPlan : & artifacts . ChunkPlanManifest {
Mode : string ( effectiveChunkCacheMode ( input . ChunkCacheMode ) ) ,
RequestedModule : pipeline . Chunk . Module ,
} ,
2026-07-07 21:10:29 +00:00
OutputEncoder : pipeline . Output . Module ,
2026-07-21 21:05:28 +00:00
ArtifactLanes : make ( [ ] artifacts . ArtifactLaneManifest , 0 , len ( pipeline . AllArtifactLanes ( ) ) ) ,
2026-07-07 21:10:29 +00:00
ValidatorChains : validatorChainManifests ( pipeline . ValidatorChains ) ,
RunID : runID ,
StartedAt : timePtr ( startedAt ) ,
References : ReferenceProvenance ( pipeline ) ,
LLMProfiles : cloneLLMProfiles ( input . LLMProfiles ) ,
2026-07-03 15:46:42 +00:00
}
2026-07-21 21:05:28 +00:00
for _ , lane := range pipeline . AllArtifactLanes ( ) {
2026-07-03 15:46:42 +00:00
laneManifest := artifacts . ArtifactLaneManifest {
2026-07-21 21:39:23 +00:00
StepID : lane . StepID ,
2026-07-03 15:46:42 +00:00
ID : lane . ID ,
Extractor : lane . Extract . Module ,
Merger : lane . Merge . Module ,
Normalizer : lane . Normalize . Module ,
}
manifest . ArtifactLanes = append ( manifest . ArtifactLanes , laneManifest )
2026-07-03 12:50:09 +00:00
}
2026-07-03 15:46:42 +00:00
return manifest
}
2026-07-03 12:50:09 +00:00
2026-07-07 21:10:29 +00:00
func validatorChainManifests ( chains [ ] ResolvedValidatorChain ) [ ] artifacts . ValidatorChainManifest {
if len ( chains ) == 0 {
return nil
}
manifests := make ( [ ] artifacts . ValidatorChainManifest , 0 , len ( chains ) )
for _ , chain := range chains {
manifest := artifacts . ValidatorChainManifest {
Stage : string ( chain . Stage ) ,
LaneID : chain . LaneID ,
ModuleKey : chain . ModuleKey ,
Validators : make ( [ ] artifacts . ValidatorManifest , 0 , len ( chain . Validators ) ) ,
}
for _ , validator := range chain . Validators {
manifest . Validators = append ( manifest . Validators , artifacts . ValidatorManifest {
Key : validator . Binding . Module ,
ExecutionClass : string ( validator . ExecutionClass ) ,
} )
}
manifests = append ( manifests , manifest )
}
return manifests
}
2026-07-03 15:46:42 +00:00
func failOutput ( output RunOutput ) RunOutput {
2026-08-27 16:40:17 +00:00
// Preserve any diagnostics already accepted by the pipeline when a later
// operation fails. Producers and validators validate each diagnostic before
// it is appended, so finalization here cannot introduce a new failure path.
_ = finalizeDiagnostics ( & output )
2026-07-03 15:46:42 +00:00
if output . Manifest . PipelineID != "" {
2026-07-17 08:13:08 +00:00
populateOutputManifest ( & output )
2026-07-03 15:46:42 +00:00
output . Manifest . ValidationStatus = "failed"
2026-07-04 00:45:21 +00:00
output . Manifest . CompletedAt = timePtr ( time . Now ( ) . UTC ( ) )
2026-07-03 15:46:42 +00:00
}
return output
}
2026-07-21 21:59:48 +00:00
func recordCheckpointEvent ( output * RunOutput , loader CheckpointLoader , stage string , stepID string , laneID string , moduleKey string , decision CheckpointDecision ) {
2026-07-08 03:02:50 +00:00
if output == nil || loader == nil || ! loader . Enabled ( ) {
return
}
2026-07-21 21:59:48 +00:00
action := checkpointDecisionCategory ( decision )
2026-07-08 03:02:50 +00:00
output . CheckpointEvents = append ( output . CheckpointEvents , CheckpointEvent {
2026-07-21 21:59:48 +00:00
Stage : stage ,
StepID : stepID ,
LaneID : laneID ,
ModuleKey : moduleKey ,
Action : action ,
Category : checkpointDecisionCategory ( decision ) ,
ReasonCode : decision . ReasonCode ,
Detail : decision . Detail ,
Reason : decision . Reason ,
2026-07-08 03:02:50 +00:00
} )
}
2026-07-22 02:13:50 +00:00
func checkpointDecisionCategory ( decision CheckpointDecision ) CheckpointDecisionCategory {
2026-07-21 21:59:48 +00:00
if decision . Category != "" {
return decision . Category
}
if decision . Reused {
2026-07-22 02:13:50 +00:00
return CheckpointDecisionReused
2026-07-21 21:59:48 +00:00
}
2026-07-22 02:13:50 +00:00
return CheckpointDecisionExecuted
2026-07-21 21:59:48 +00:00
}
2026-07-17 08:13:08 +00:00
func populateOutputManifest ( output * RunOutput ) {
2026-07-07 19:27:28 +00:00
if output == nil {
return
}
output . Manifest . NormalizedOutputs = normalizedOutputManifests ( output . NormalizeOutputs )
output . Manifest . RejectedOutputs = rejectedOutputManifests ( output . Rejected )
2026-08-27 01:27:25 +00:00
if len ( output . ValidationSummaries ) > 0 {
output . Manifest . ValidationSummaries = cloneValidationSummaries ( output . ValidationSummaries )
}
2026-07-21 21:59:48 +00:00
if len ( output . CheckpointEvents ) > 0 {
decisions := make ( [ ] artifacts . CheckpointDecisionManifest , 0 , len ( output . CheckpointEvents ) )
for _ , event := range output . CheckpointEvents {
2026-07-22 02:13:50 +00:00
decisions = append ( decisions , artifacts . CheckpointDecisionManifest { Stage : event . Stage , StepID : event . StepID , LaneID : event . LaneID , ModuleKey : event . ModuleKey , Category : string ( event . Category ) , ReasonCode : string ( event . ReasonCode ) , Detail : event . Detail } )
2026-07-21 21:59:48 +00:00
}
output . Manifest . CheckpointDecisions = decisions
}
2026-07-07 19:27:28 +00:00
}
2026-07-17 07:33:49 +00:00
func normalizedOutputManifests ( outputs [ ] contracts . SerializedOutput ) [ ] artifacts . NormalizedOutputManifest {
2026-07-07 19:27:28 +00:00
if len ( outputs ) == 0 {
return nil
}
manifests := make ( [ ] artifacts . NormalizedOutputManifest , 0 , len ( outputs ) )
for _ , output := range outputs {
manifests = append ( manifests , artifacts . NormalizedOutputManifest {
2026-07-21 21:39:23 +00:00
StepID : output . StepID ,
2026-07-07 19:27:28 +00:00
LaneID : output . LaneID ,
ModuleKey : output . NormalizerKey ,
SourceID : output . SourceID ,
2026-07-17 07:33:49 +00:00
MediaType : output . Artifact . MediaType ,
2026-07-07 19:27:28 +00:00
Schema : artifacts . OutputSchemaProvenance {
2026-07-17 07:33:49 +00:00
ID : output . Artifact . Schema . ID ,
Name : output . Artifact . Schema . Name ,
Version : output . Artifact . Schema . Version ,
2026-07-07 19:27:28 +00:00
} ,
} )
}
return manifests
}
func rejectedOutputManifests ( rejected [ ] contracts . RejectedOutput ) [ ] artifacts . RejectedOutputManifest {
if len ( rejected ) == 0 {
return nil
}
manifests := make ( [ ] artifacts . RejectedOutputManifest , 0 , len ( rejected ) )
for _ , output := range rejected {
manifests = append ( manifests , artifacts . RejectedOutputManifest {
Stage : output . Stage ,
2026-07-21 21:39:23 +00:00
StepID : output . StepID ,
2026-07-07 19:27:28 +00:00
LaneID : output . LaneID ,
ModuleKey : output . ModuleKey ,
ChunkID : output . ChunkID ,
ChunkIndex : output . ChunkIndex ,
ValidatorName : output . ValidatorName ,
ReasonCode : output . ReasonCode ,
Message : output . Message ,
AttemptCount : output . AttemptCount ,
DiagnosticArtifactPath : output . DiagnosticArtifactPath ,
2026-08-27 01:27:25 +00:00
Validation : cloneValidationSummaryPtr ( output . Validation ) ,
2026-07-07 19:27:28 +00:00
} )
}
return manifests
}
2026-07-17 23:16:16 -05:00
func attachModuleManifestMetadata ( output * RunOutput , moduleKey string , module any ) error {
2026-07-05 13:23:56 +00:00
if output == nil {
2026-07-17 23:16:16 -05:00
return nil
}
metadata , ok , err := moduleManifestMetadata ( module )
if err != nil {
return fmt . Errorf ( "clone manifest metadata for module %q: %w" , moduleKey , err )
2026-07-05 13:23:56 +00:00
}
if ! ok {
2026-07-17 23:16:16 -05:00
return nil
2026-07-05 13:23:56 +00:00
}
if output . Manifest . ModuleMetadata == nil {
output . Manifest . ModuleMetadata = make ( map [ string ] map [ string ] any )
}
2026-07-17 08:13:08 +00:00
output . Manifest . ModuleMetadata [ moduleKey ] = metadata
2026-07-17 23:16:16 -05:00
return nil
2026-07-04 00:45:21 +00:00
}
2026-07-17 23:16:16 -05:00
func moduleManifestMetadata ( module any ) ( map [ string ] any , bool , error ) {
2026-07-05 13:23:56 +00:00
provider , ok := module . ( contracts . ManifestMetadataProvider )
if ! ok {
2026-07-17 23:16:16 -05:00
return nil , false , nil
2026-07-05 13:23:56 +00:00
}
2026-07-17 23:16:16 -05:00
moduleMetadata , err := cloneMetadata ( provider . ManifestMetadata ( ) )
if err != nil {
return nil , false , err
}
2026-07-05 13:23:56 +00:00
if len ( moduleMetadata ) == 0 {
2026-07-17 23:16:16 -05:00
return nil , false , nil
2026-07-05 13:23:56 +00:00
}
2026-07-17 23:16:16 -05:00
return moduleMetadata , true , nil
2026-07-05 13:23:56 +00:00
}
2026-07-04 00:45:21 +00:00
func outputFilesFromResult ( result contracts . OutputResult ) ( [ ] contracts . OutputFile , error ) {
2026-07-03 21:26:05 -05:00
out := make ( [ ] contracts . OutputFile , 0 , len ( result . Files ) )
for _ , file := range result . Files {
2026-07-04 00:45:21 +00:00
if err := validateOutputFileName ( file . Name ) ; err != nil {
return nil , err
}
out = append ( out , contracts . OutputFile {
Name : file . Name ,
ContentType : file . ContentType ,
Bytes : append ( [ ] byte ( nil ) , file . Bytes ... ) ,
} )
}
return out , nil
}
func validateOutputFileName ( name string ) error {
if strings . TrimSpace ( name ) == "" {
return fmt . Errorf ( "output file name must not be empty" )
}
if strings . Contains ( name , "\\" ) {
return fmt . Errorf ( "output file name %q must use slash-separated relative paths" , name )
}
if path . IsAbs ( name ) {
return fmt . Errorf ( "output file name %q must be relative" , name )
}
if strings . Contains ( name , ".." ) {
return fmt . Errorf ( "output file name %q must not contain .." , name )
}
cleaned := path . Clean ( name )
if cleaned == "." || cleaned != name {
return fmt . Errorf ( "output file name %q must be clean" , name )
}
return nil
}
2026-07-17 23:16:16 -05:00
func cloneMetadata ( metadata map [ string ] any ) ( map [ string ] any , error ) {
return source . CloneMetadata ( metadata )
2026-07-18 00:45:01 +00:00
}
2026-07-04 00:45:21 +00:00
func cloneLLMProfiles ( profiles [ ] artifacts . LLMProfileManifest ) [ ] artifacts . LLMProfileManifest {
if len ( profiles ) == 0 {
return nil
}
return append ( [ ] artifacts . LLMProfileManifest ( nil ) , profiles ... )
}
2026-07-05 18:21:37 +00:00
func llmProfileManifests ( client contracts . StructuredLLMClient ) [ ] artifacts . LLMProfileManifest {
provider , ok := client . ( contracts . LLMProfileManifestProvider )
if ! ok {
return nil
}
return provider . LLMProfileManifests ( )
}
func mergeLLMProfileManifests ( sources ... [ ] artifacts . LLMProfileManifest ) [ ] artifacts . LLMProfileManifest {
merged := make ( map [ string ] artifacts . LLMProfileManifest )
for _ , source := range sources {
for _ , profile := range source {
2026-07-30 02:58:25 +00:00
profile = profile . Normalized ( )
key := profile . IdentityKey ( )
2026-07-05 18:21:37 +00:00
if _ , exists := merged [ key ] ; exists {
continue
}
2026-07-30 02:58:25 +00:00
merged [ key ] = profile
2026-07-05 18:21:37 +00:00
}
}
if len ( merged ) == 0 {
return nil
}
keys := make ( [ ] string , 0 , len ( merged ) )
for key := range merged {
keys = append ( keys , key )
}
sort . Strings ( keys )
out := make ( [ ] artifacts . LLMProfileManifest , 0 , len ( keys ) )
for _ , key := range keys {
out = append ( out , merged [ key ] )
}
return out
}
2026-07-05 17:51:36 +00:00
func sourceInputMaterial ( inputPath string , content [ ] byte ) contracts . LLMInputMaterial {
return contracts . NewLLMInputMaterial (
"source" ,
sourceInputMediaType ( inputPath ) ,
content ,
sourceInputDigest ( content ) ,
sourceInputOriginURI ( inputPath ) ,
)
}
2026-07-17 05:32:11 +00:00
func chunkInputMaterial ( sourceInput contracts . LLMInputMaterial , chunk source . Chunk ) contracts . LLMInputMaterial {
2026-07-07 15:32:35 -05:00
return contracts . NewLLMInputMaterial (
"source" ,
chunk . MediaType ,
chunk . Content ,
sourceInputDigest ( chunk . Content ) ,
sourceInput . OriginURI ,
)
}
2026-07-05 17:51:36 +00:00
func sourceInputMediaType ( inputPath string ) string {
extension := strings . ToLower ( filepath . Ext ( strings . TrimSpace ( inputPath ) ) )
if extension == ".json" {
return "application/json"
}
mediaType := mime . TypeByExtension ( extension )
if strings . TrimSpace ( mediaType ) == "" {
return unknownMediaType
}
return canonicalMediaType ( mediaType )
}
func sourceInputDigest ( content [ ] byte ) string {
sum := sha256 . Sum256 ( content )
return "sha256:" + hex . EncodeToString ( sum [ : ] )
}
func sourceInputOriginURI ( inputPath string ) string {
if strings . TrimSpace ( inputPath ) == "" {
return ""
}
return fileURI ( inputPath )
}
2026-07-17 23:16:16 -05:00
func manifestMetadataWithSessionID ( metadata map [ string ] any , sessionID string ) ( map [ string ] any , error ) {
out , err := cloneMetadata ( metadata )
if err != nil {
return nil , fmt . Errorf ( "clone run manifest metadata: %w" , err )
}
2026-07-05 17:51:36 +00:00
if strings . TrimSpace ( sessionID ) == "" {
2026-07-17 23:16:16 -05:00
return out , nil
2026-07-05 17:51:36 +00:00
}
if out == nil {
out = make ( map [ string ] any )
}
out [ "session_id" ] = sessionID
2026-07-17 23:16:16 -05:00
return out , nil
2026-07-05 17:51:36 +00:00
}
2026-07-17 23:16:16 -05:00
func cloneSourceChunkPtr ( chunk * source . Chunk ) ( * source . Chunk , error ) {
2026-07-07 21:27:28 +00:00
if chunk == nil {
2026-07-17 23:16:16 -05:00
return nil , nil
}
cloned , err := cloneSourceChunk ( * chunk )
if err != nil {
return nil , err
2026-07-07 21:27:28 +00:00
}
2026-07-17 23:16:16 -05:00
return & cloned , nil
2026-07-07 21:27:28 +00:00
}
2026-07-17 23:16:16 -05:00
func cloneSourceChunk ( chunk source . Chunk ) ( source . Chunk , error ) {
2026-07-07 21:27:28 +00:00
chunk . Content = append ( [ ] byte ( nil ) , chunk . Content ... )
2026-07-17 23:16:16 -05:00
var err error
chunk . Units , err = cloneSourceUnits ( chunk . Units )
if err != nil {
return source . Chunk { } , err
}
chunk . Metadata , err = cloneMetadata ( chunk . Metadata )
if err != nil {
return source . Chunk { } , fmt . Errorf ( "clone chunk metadata: %w" , err )
}
2026-07-17 23:47:22 +00:00
chunk . Annotations = source . CloneChunkAnnotations ( chunk . Annotations )
chunk . PlanAnnotations = source . CloneChunkAnnotations ( chunk . PlanAnnotations )
2026-07-17 23:16:16 -05:00
return chunk , nil
2026-07-07 21:27:28 +00:00
}
2026-07-17 23:16:16 -05:00
func cloneSourceChunks ( chunks [ ] source . Chunk ) ( [ ] source . Chunk , error ) {
2026-07-07 21:27:28 +00:00
if len ( chunks ) == 0 {
2026-07-17 23:16:16 -05:00
return nil , nil
2026-07-07 21:27:28 +00:00
}
2026-07-17 05:32:11 +00:00
out := make ( [ ] source . Chunk , 0 , len ( chunks ) )
2026-07-07 21:27:28 +00:00
for _ , chunk := range chunks {
2026-07-17 23:16:16 -05:00
cloned , err := cloneSourceChunk ( chunk )
if err != nil {
return nil , err
}
out = append ( out , cloned )
2026-07-07 21:27:28 +00:00
}
2026-07-17 23:16:16 -05:00
return out , nil
2026-07-07 21:27:28 +00:00
}
2026-07-17 23:16:16 -05:00
func cloneSourceUnits ( units [ ] source . SourceUnit ) ( [ ] source . SourceUnit , error ) {
2026-07-07 21:27:28 +00:00
if len ( units ) == 0 {
2026-07-17 23:16:16 -05:00
return nil , nil
2026-07-07 21:27:28 +00:00
}
out := make ( [ ] source . SourceUnit , 0 , len ( units ) )
for _ , unit := range units {
2026-07-17 23:16:16 -05:00
cloned , err := cloneSourceUnit ( unit )
if err != nil {
return nil , err
}
out = append ( out , cloned )
2026-07-07 21:27:28 +00:00
}
2026-07-17 23:16:16 -05:00
return out , nil
2026-07-07 21:27:28 +00:00
}
2026-07-17 07:33:49 +00:00
func cloneSerializedOutputs ( outputs [ ] contracts . SerializedOutput ) [ ] contracts . SerializedOutput {
2026-07-07 18:58:23 +00:00
if len ( outputs ) == 0 {
return nil
2026-07-03 12:50:09 +00:00
}
2026-07-17 07:33:49 +00:00
out := make ( [ ] contracts . SerializedOutput , 0 , len ( outputs ) )
2026-07-07 18:58:23 +00:00
for _ , output := range outputs {
2026-07-17 07:33:49 +00:00
out = append ( out , contracts . CloneSerializedOutput ( output ) )
2026-07-07 18:58:23 +00:00
}
return out
2026-07-03 12:50:09 +00:00
}
2026-07-07 18:58:23 +00:00
func cloneRejectedOutputs ( rejected [ ] contracts . RejectedOutput ) [ ] contracts . RejectedOutput {
if len ( rejected ) == 0 {
return nil
2026-07-03 11:26:49 -05:00
}
2026-08-27 01:27:25 +00:00
cloned := make ( [ ] contracts . RejectedOutput , len ( rejected ) )
for index , item := range rejected {
cloned [ index ] = item
cloned [ index ] . Validation = cloneValidationSummaryPtr ( item . Validation )
}
return cloned
}
func cloneValidationSummaryPtr ( summary * artifacts . ValidationSummary ) * artifacts . ValidationSummary {
if summary == nil {
return nil
}
cloned := artifacts . CloneValidationSummary ( * summary )
return & cloned
}
func cloneValidationSummaries ( summaries [ ] artifacts . ValidationSummary ) [ ] artifacts . ValidationSummary {
if len ( summaries ) == 0 {
return nil
}
cloned := make ( [ ] artifacts . ValidationSummary , len ( summaries ) )
for index , summary := range summaries {
cloned [ index ] = artifacts . CloneValidationSummary ( summary )
}
return cloned
2026-07-03 11:26:49 -05:00
}
2026-07-07 18:58:23 +00:00
func timePtr ( t time . Time ) * time . Time {
return & t
}