package source import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" ) // DigestDocument returns a deterministic digest of the canonical source // document content. The existing Digest field is excluded from its own digest. func DigestDocument(doc *SourceDocument) (string, error) { if doc == nil { return "", fmt.Errorf("source document must not be nil") } payload := struct { ID string `json:"id"` Kind string `json:"kind"` Format string `json:"format"` Units []SourceUnit `json:"units"` Metadata map[string]any `json:"metadata,omitempty"` }{ ID: doc.ID, Kind: doc.Kind, Format: doc.Format, Units: doc.Units, Metadata: doc.Metadata, } encoded, err := json.Marshal(payload) if err != nil { return "", fmt.Errorf("encode source document for digest: %w", err) } sum := sha256.Sum256(encoded) return "sha256:" + hex.EncodeToString(sum[:]), nil } // DigestChunk returns a deterministic digest of a chunk, including its source // provenance, content, units, and metadata. func DigestChunk(chunk Chunk) (string, error) { annotations, err := CanonicalizeChunkAnnotations(chunk.Annotations) if err != nil { return "", fmt.Errorf("canonicalize source chunk annotations: %w", err) } planAnnotations, err := CanonicalizeChunkAnnotations(chunk.PlanAnnotations) if err != nil { return "", fmt.Errorf("canonicalize source chunk plan annotations: %w", err) } payload := struct { ID string `json:"id"` SourceID string `json:"source_id"` Index int `json:"index"` Ref SourceRef `json:"ref"` Content []byte `json:"content"` MediaType string `json:"media_type"` Units []SourceUnit `json:"units"` Metadata map[string]any `json:"metadata,omitempty"` Annotations ChunkAnnotations `json:"annotations,omitempty"` PlanAnnotations ChunkAnnotations `json:"plan_annotations,omitempty"` }{ ID: chunk.ID, SourceID: chunk.SourceID, Index: chunk.Index, Ref: chunk.Ref, Content: chunk.Content, MediaType: chunk.MediaType, Units: chunk.Units, Metadata: chunk.Metadata, Annotations: annotations, PlanAnnotations: planAnnotations, } encoded, err := json.Marshal(payload) if err != nil { return "", fmt.Errorf("encode source chunk for digest: %w", err) } sum := sha256.Sum256(encoded) return "sha256:" + hex.EncodeToString(sum[:]), nil } // DigestChunkPlan returns a deterministic digest of the logical plan. Storage // schema, producer provenance, warnings, and timestamps are intentionally not // part of the digest. func DigestChunkPlan(plan ChunkPlan) (string, error) { canonical, err := CanonicalizeChunkPlan(plan) if err != nil { return "", err } if isBlank(canonical.SourceDigest) { return "", fmt.Errorf("chunk plan source_digest must not be empty") } if hasSurroundingWhitespace(canonical.SourceDigest) { return "", fmt.Errorf("chunk plan source_digest must not contain leading or trailing whitespace") } if len(canonical.Ranges) == 0 { return "", fmt.Errorf("chunk plan ranges must not be empty") } encoded, err := json.Marshal(canonical) if err != nil { return "", fmt.Errorf("encode chunk plan for digest: %w", err) } sum := sha256.Sum256(encoded) return "sha256:" + hex.EncodeToString(sum[:]), nil }