package pipeline import ( "fmt" "strings" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/evidencecontext" ) // debugEvidenceContextSummary intentionally contains only publication-safe // identifiers and aggregate counts. The evidence document itself can include // source text and must never be written to this debug envelope. type debugEvidenceContextSummary struct { ArtifactKind contracts.ArtifactKind `json:"artifact_kind"` MediaType string `json:"media_type"` SchemaID string `json:"schema_id"` SchemaName string `json:"schema_name"` SchemaVersion string `json:"schema_version"` SelectedLanes []string `json:"selected_lanes"` WindowUnits int `json:"window_units"` UnitCount int `json:"unit_count"` SourceDigest string `json:"source_digest"` } // buildOutputEvidenceContext projects the prepared output policy from accepted // normalized artifacts. It is intentionally separate from lane execution so // checkpointed normalized outputs use the same reconstruction path. func buildOutputEvidenceContext(prepared *PreparedPipeline, doc *source.SourceDocument, outputs []contracts.SerializedOutput) (*contracts.SerializedArtifact, *debugEvidenceContextSummary, error) { if prepared == nil || prepared.evidencePlan == nil { return nil, nil, nil } if doc == nil { return nil, nil, fmt.Errorf("evidence context output: source document is unavailable") } if prepared.artifactCodecs == nil { return nil, nil, fmt.Errorf("evidence context output: artifact codecs are unavailable") } byLane := make(map[string]contracts.SerializedOutput, len(outputs)) for _, output := range outputs { laneID := strings.TrimSpace(output.LaneID) if _, exists := byLane[laneID]; exists { return nil, nil, fmt.Errorf("evidence context output: accepted normalized outputs contain duplicate lane %q", laneID) } byLane[laneID] = contracts.CloneSerializedOutput(output) } request := evidencecontext.BuildRequest{ Source: doc, WindowUnits: prepared.evidencePlan.policy.WindowUnits, SourceRefs: make([]source.SourceRef, 0), } for _, lane := range prepared.evidencePlan.lanes { output, ok := byLane[lane.laneID] if !ok { continue } if output.SourceID != doc.ID { return nil, nil, fmt.Errorf("evidence context output lane %q: accepted normalized output source is incompatible", lane.laneID) } if output.Artifact.Kind != lane.kind { return nil, nil, fmt.Errorf("evidence context output lane %q: accepted normalized output artifact kind is incompatible", lane.laneID) } value, err := prepared.artifactCodecs.Decode(contracts.CloneSerializedArtifact(output.Artifact)) if err != nil { return nil, nil, fmt.Errorf("evidence context output lane %q: accepted normalized artifact cannot be decoded", lane.laneID) } references, err := lane.project(value) if err != nil { return nil, nil, fmt.Errorf("evidence context output lane %q: accepted normalized artifact cannot be projected", lane.laneID) } request.SourceRefs = append(request.SourceRefs, references...) } document, err := evidencecontext.Build(request) if err != nil { return nil, nil, fmt.Errorf("evidence context output: accepted evidence references are invalid") } content, err := evidencecontext.New().Encode(document) if err != nil { return nil, nil, fmt.Errorf("evidence context output: evidence context serialization failed") } artifact := &contracts.SerializedArtifact{ Kind: evidencecontext.ArtifactKind, Schema: evidencecontext.New().Schema(), MediaType: evidencecontext.MediaType, Content: content, } summary := debugEvidenceContextSummary{ ArtifactKind: artifact.Kind, MediaType: artifact.MediaType, SchemaID: artifact.Schema.ID, SchemaName: artifact.Schema.Name, SchemaVersion: artifact.Schema.Version, SelectedLanes: append([]string(nil), prepared.evidencePlan.policy.LaneIDs...), WindowUnits: prepared.evidencePlan.policy.WindowUnits, SourceDigest: doc.Digest, UnitCount: len(document), } return contracts.CloneSerializedArtifactPointer(artifact), &summary, nil }