Files
notarius/internal/framework/pipeline/evidence_output.go

104 lines
4.3 KiB
Go

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
}