2026-07-08 03:02:50 +00:00
package checkpoint
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
2026-07-18 04:55:33 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/fileio"
2026-07-08 03:02:50 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
2026-07-18 04:55:33 +00:00
type FilesystemLoader struct {
2026-07-08 03:02:50 +00:00
root string
identityDigest string
}
2026-07-18 04:55:33 +00:00
func NewFilesystemLoader ( root string , identity Identity ) ( pipeline . CheckpointLoader , error ) {
root = strings . TrimSpace ( root )
if root == "" {
return pipeline . NoopCheckpointLoader ( ) , nil
}
relative , err := identity . RelativePath ( )
2026-07-08 03:02:50 +00:00
if err != nil {
return nil , err
}
2026-07-18 04:55:33 +00:00
target , err := fileio . SafePath ( root , relative )
if err != nil {
return nil , err
2026-07-08 03:02:50 +00:00
}
2026-07-18 04:55:33 +00:00
return & FilesystemLoader { root : target , identityDigest : identity . Digest } , nil
2026-07-08 03:02:50 +00:00
}
2026-07-18 04:55:33 +00:00
func ( l * FilesystemLoader ) Enabled ( ) bool {
2026-07-08 03:02:50 +00:00
return l != nil && strings . TrimSpace ( l . root ) != ""
}
2026-07-18 04:55:33 +00:00
func ( l * FilesystemLoader ) Source ( moduleKey string ) ( pipeline . SourceCheckpoint , pipeline . CheckpointDecision ) {
var manifest SourceManifest
2026-07-08 03:02:50 +00:00
if decision := l . readJSON ( "source/manifest.json" , & manifest ) ; ! decision . Reused {
return pipeline . SourceCheckpoint { } , decision
}
2026-07-18 04:55:33 +00:00
if decision := l . validateManifest ( manifest . StageManifest , StageSource , "" , moduleKey , StatusSucceeded , nil ) ; ! decision . Reused {
2026-07-08 03:02:50 +00:00
return pipeline . SourceCheckpoint { } , decision
}
var payload sourceDocumentEnvelope
if decision := l . readJSON ( "source/source-document.json" , & payload ) ; ! decision . Reused {
return pipeline . SourceCheckpoint { } , decision
}
doc := cloneSourceDocument ( payload . Document )
if err := source . ValidateDocument ( & doc ) ; err != nil {
return pipeline . SourceCheckpoint { } , invalidDecision ( "source checkpoint document is invalid: %v" , err )
}
if strings . TrimSpace ( manifest . SourceID ) != "" && manifest . SourceID != doc . ID {
return pipeline . SourceCheckpoint { } , invalidDecision ( "source checkpoint source id does not match payload" )
}
2026-07-18 04:55:33 +00:00
if ! fingerprintsEqual ( checkpointToPipelineFingerprints ( manifest . OutputDigests ) , digestFingerprints ( "source_document" , doc . Digest ) ) {
2026-07-08 03:02:50 +00:00
return pipeline . SourceCheckpoint { } , invalidDecision ( "source checkpoint output digest does not match payload" )
}
return pipeline . SourceCheckpoint { Document : & doc } , reusedDecision ( )
}
2026-07-18 04:55:33 +00:00
func ( l * FilesystemLoader ) Extract ( laneID , moduleKey string , dependencies [ ] pipeline . CheckpointFingerprint ) ( pipeline . ExtractCheckpoint , pipeline . CheckpointDecision ) {
2026-07-21 21:59:48 +00:00
return l . ExtractForStep ( "" , laneID , moduleKey , dependencies )
}
func ( l * FilesystemLoader ) ExtractForStep ( stepID , laneID , moduleKey string , dependencies [ ] pipeline . CheckpointFingerprint ) ( pipeline . ExtractCheckpoint , pipeline . CheckpointDecision ) {
2026-07-18 04:55:33 +00:00
var manifest ExtractLaneManifest
2026-07-21 21:59:48 +00:00
if d := l . readJSON ( laneManifestPath ( "extract" , stepID , laneID ) , & manifest ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . ExtractCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
2026-07-21 21:59:48 +00:00
if d := l . validateLaneManifest ( manifest . StageManifest , StageExtract , stepID , laneID , moduleKey , dependencies , StatusSucceeded , StatusSucceededWithRejections ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . ExtractCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
var payload artifactExtractEnvelope
2026-07-21 21:59:48 +00:00
if d := l . readJSON ( lanePayloadPath ( "extract" , stepID , laneID , "outputs.json" ) , & payload ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . ExtractCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
outputs , err := artifactCheckpointOutputs ( payload . Outputs )
if err != nil {
2026-07-17 08:13:08 +00:00
return pipeline . ExtractCheckpoint { } , invalidDecision ( "extract artifact checkpoint payload is invalid: %v" , err )
2026-07-17 07:33:49 +00:00
}
2026-07-18 04:55:33 +00:00
if ! fingerprintsEqual ( checkpointToPipelineFingerprints ( manifest . OutputDigests ) , artifactOutputDigests ( outputs ) ) {
2026-07-17 08:13:08 +00:00
return pipeline . ExtractCheckpoint { } , invalidDecision ( "extract artifact checkpoint output digests do not match payload" )
2026-07-08 03:02:50 +00:00
}
2026-07-17 08:13:08 +00:00
return pipeline . ExtractCheckpoint { Outputs : outputs , Rejected : cloneRejectedOutputs ( payload . Rejected ) , Warnings : cloneWarnings ( payload . Warnings ) } , reusedDecision ( )
2026-07-08 03:02:50 +00:00
}
2026-07-18 04:55:33 +00:00
func ( l * FilesystemLoader ) Merge ( laneID , moduleKey string , dependencies [ ] pipeline . CheckpointFingerprint ) ( pipeline . MergeCheckpoint , pipeline . CheckpointDecision ) {
2026-07-21 21:59:48 +00:00
return l . MergeForStep ( "" , laneID , moduleKey , dependencies )
}
func ( l * FilesystemLoader ) MergeForStep ( stepID , laneID , moduleKey string , dependencies [ ] pipeline . CheckpointFingerprint ) ( pipeline . MergeCheckpoint , pipeline . CheckpointDecision ) {
2026-07-18 04:55:33 +00:00
var manifest MergeLaneManifest
2026-07-21 21:59:48 +00:00
if d := l . readJSON ( laneManifestPath ( "merge" , stepID , laneID ) , & manifest ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . MergeCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
2026-07-21 21:59:48 +00:00
if d := l . validateLaneManifest ( manifest . StageManifest , StageMerge , stepID , laneID , moduleKey , dependencies , StatusSucceeded ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . MergeCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
var payload artifactSingleEnvelope
2026-07-21 21:59:48 +00:00
if d := l . readJSON ( lanePayloadPath ( "merge" , stepID , laneID , "output.json" ) , & payload ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . MergeCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
values , err := artifactCheckpointOutputs ( [ ] artifactCheckpointEnvelope { payload . Output } )
if err != nil {
2026-07-17 08:13:08 +00:00
return pipeline . MergeCheckpoint { } , invalidDecision ( "merge artifact checkpoint payload is invalid: %v" , err )
2026-07-17 07:33:49 +00:00
}
2026-07-18 04:55:33 +00:00
if ! fingerprintsEqual ( checkpointToPipelineFingerprints ( manifest . OutputDigests ) , artifactOutputDigests ( values ) ) {
2026-07-17 08:13:08 +00:00
return pipeline . MergeCheckpoint { } , invalidDecision ( "merge artifact checkpoint output digest does not match payload" )
2026-07-08 03:02:50 +00:00
}
2026-07-17 08:13:08 +00:00
return pipeline . MergeCheckpoint { Output : values [ 0 ] , Warnings : cloneWarnings ( payload . Warnings ) } , reusedDecision ( )
2026-07-08 03:02:50 +00:00
}
2026-07-18 04:55:33 +00:00
func ( l * FilesystemLoader ) Normalize ( laneID , moduleKey string , dependencies [ ] pipeline . CheckpointFingerprint ) ( pipeline . NormalizeCheckpoint , pipeline . CheckpointDecision ) {
2026-07-21 21:59:48 +00:00
return l . NormalizeForStep ( "" , laneID , moduleKey , dependencies )
}
func ( l * FilesystemLoader ) NormalizeForStep ( stepID , laneID , moduleKey string , dependencies [ ] pipeline . CheckpointFingerprint ) ( pipeline . NormalizeCheckpoint , pipeline . CheckpointDecision ) {
2026-07-18 04:55:33 +00:00
var manifest NormalizeLaneManifest
2026-07-21 21:59:48 +00:00
if d := l . readJSON ( laneManifestPath ( "normalize" , stepID , laneID ) , & manifest ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . NormalizeCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
2026-07-21 21:59:48 +00:00
if d := l . validateLaneManifest ( manifest . StageManifest , StageNormalize , stepID , laneID , moduleKey , dependencies , StatusSucceeded ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . NormalizeCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
var payload artifactSingleEnvelope
2026-07-21 21:59:48 +00:00
if d := l . readJSON ( lanePayloadPath ( "normalize" , stepID , laneID , "output.json" ) , & payload ) ; ! d . Reused {
2026-07-17 08:13:08 +00:00
return pipeline . NormalizeCheckpoint { } , d
2026-07-17 07:33:49 +00:00
}
values , err := artifactCheckpointOutputs ( [ ] artifactCheckpointEnvelope { payload . Output } )
if err != nil {
2026-07-17 08:13:08 +00:00
return pipeline . NormalizeCheckpoint { } , invalidDecision ( "normalize artifact checkpoint payload is invalid: %v" , err )
2026-07-17 07:33:49 +00:00
}
2026-07-18 04:55:33 +00:00
if ! fingerprintsEqual ( checkpointToPipelineFingerprints ( manifest . OutputDigests ) , artifactOutputDigests ( values ) ) {
2026-07-17 08:13:08 +00:00
return pipeline . NormalizeCheckpoint { } , invalidDecision ( "normalize artifact checkpoint output digest does not match payload" )
2026-07-17 07:33:49 +00:00
}
2026-07-17 08:13:08 +00:00
return pipeline . NormalizeCheckpoint { Output : values [ 0 ] , Warnings : cloneWarnings ( payload . Warnings ) } , reusedDecision ( )
2026-07-17 07:33:49 +00:00
}
2026-07-17 08:13:08 +00:00
func artifactCheckpointOutputs ( values [ ] artifactCheckpointEnvelope ) ( [ ] pipeline . CheckpointArtifact , error ) {
2026-07-17 07:33:49 +00:00
if len ( values ) == 0 {
return nil , nil
}
2026-07-17 08:13:08 +00:00
out := make ( [ ] pipeline . CheckpointArtifact , 0 , len ( values ) )
2026-07-17 07:33:49 +00:00
for _ , v := range values {
content , err := contentFromEnvelope ( v . Content )
if err != nil {
return nil , err
}
if strings . TrimSpace ( string ( v . Kind ) ) == "" || strings . TrimSpace ( v . Schema . ID ) == "" || strings . TrimSpace ( v . Schema . Version ) == "" || strings . TrimSpace ( v . SchemaDigest ) == "" {
return nil , fmt . Errorf ( "artifact codec identity is incomplete" )
}
2026-07-17 08:13:08 +00:00
out = append ( out , pipeline . CheckpointArtifact { LaneID : v . LaneID , ModuleKey : v . ModuleKey , SourceID : v . SourceID , ChunkID : v . ChunkID , ChunkIndex : v . ChunkIndex , ChunkRef : v . ChunkRef , SchemaDigest : v . SchemaDigest , Artifact : contracts . SerializedArtifact { Kind : v . Kind , Schema : v . Schema , MediaType : v . Content . MediaType , Content : content , Metadata : cloneMetadata ( v . Content . Metadata ) } } )
2026-07-17 07:33:49 +00:00
}
return out , nil
}
2026-07-18 04:55:33 +00:00
func ( l * FilesystemLoader ) readJSON ( name string , out any ) pipeline . CheckpointDecision {
2026-07-08 03:02:50 +00:00
if ! l . Enabled ( ) {
2026-07-21 21:59:48 +00:00
return pipeline . CheckpointDecision { Category : "executed" , ReasonCode : "loading_disabled" , Reason : "checkpoint loading disabled" }
2026-07-08 03:02:50 +00:00
}
2026-07-18 04:55:33 +00:00
target , err := fileio . SafePath ( l . root , name )
2026-07-08 03:02:50 +00:00
if err != nil {
return invalidDecision ( "checkpoint path is invalid: %v" , err )
}
data , err := os . ReadFile ( target )
if err != nil {
if os . IsNotExist ( err ) {
2026-07-21 21:59:48 +00:00
return pipeline . CheckpointDecision { Category : "executed" , ReasonCode : "checkpoint_missing" , Reason : "checkpoint artifact is missing" }
2026-07-08 03:02:50 +00:00
}
return invalidDecision ( "read checkpoint artifact: %v" , err )
}
if err := json . Unmarshal ( data , out ) ; err != nil {
return invalidDecision ( "decode checkpoint artifact: %v" , err )
}
return reusedDecision ( )
}
2026-07-18 04:55:33 +00:00
func ( l * FilesystemLoader ) validateManifest ( manifest StageManifest , stage StageName , laneID string , moduleKey string , status StageStatus , dependencies [ ] pipeline . CheckpointFingerprint ) pipeline . CheckpointDecision {
2026-07-21 21:59:48 +00:00
return l . validateLaneManifest ( manifest , stage , "" , laneID , moduleKey , dependencies , status )
2026-07-08 03:02:50 +00:00
}
2026-07-21 21:59:48 +00:00
func ( l * FilesystemLoader ) validateLaneManifest ( manifest StageManifest , stage StageName , stepID string , laneID string , moduleKey string , dependencies [ ] pipeline . CheckpointFingerprint , statuses ... StageStatus ) pipeline . CheckpointDecision {
2026-07-18 04:55:33 +00:00
if manifest . WorkspaceSchemaVersion == WorkspaceSchemaVersionV1 {
return invalidDecision ( "checkpoint workspace schema version %q is incompatible with %q and must be recomputed" , manifest . WorkspaceSchemaVersion , WorkspaceSchemaVersion )
2026-07-17 05:32:11 +00:00
}
2026-07-21 21:59:48 +00:00
if manifest . WorkspaceSchemaVersion == WorkspaceSchemaVersionV2 {
return invalidDecision ( "checkpoint workspace schema version %q is incompatible with %q and must be recomputed" , manifest . WorkspaceSchemaVersion , WorkspaceSchemaVersion )
}
2026-07-18 04:55:33 +00:00
if manifest . WorkspaceSchemaVersion != WorkspaceSchemaVersion {
2026-07-08 03:02:50 +00:00
return invalidDecision ( "checkpoint workspace schema version %q is not supported" , manifest . WorkspaceSchemaVersion )
}
if strings . TrimSpace ( l . identityDigest ) != "" && manifest . Metadata [ "checkpoint_identity_digest" ] != l . identityDigest {
return invalidDecision ( "checkpoint identity digest does not match current invocation" )
}
if manifest . Stage != stage {
return invalidDecision ( "checkpoint stage %q does not match %q" , manifest . Stage , stage )
}
2026-07-21 21:59:48 +00:00
if strings . TrimSpace ( stepID ) != "" && manifest . StepID != stepID {
return invalidDecision ( "checkpoint step does not match requested step" )
}
2026-07-08 03:02:50 +00:00
if strings . TrimSpace ( laneID ) != "" && manifest . LaneID != laneID {
return invalidDecision ( "checkpoint lane %q does not match %q" , manifest . LaneID , laneID )
}
if strings . TrimSpace ( moduleKey ) != "" && manifest . ModuleKey != moduleKey {
return invalidDecision ( "checkpoint module %q does not match %q" , manifest . ModuleKey , moduleKey )
}
statusOK := false
for _ , status := range statuses {
if manifest . Status == status {
statusOK = true
break
}
}
if ! statusOK {
return invalidDecision ( "checkpoint status %q cannot be reused" , manifest . Status )
}
2026-07-18 04:55:33 +00:00
if ! fingerprintsEqual ( checkpointToPipelineFingerprints ( manifest . DependencyFingerprints ) , dependencies ) {
2026-07-08 03:02:50 +00:00
return invalidDecision ( "checkpoint dependency fingerprints do not match" )
}
return reusedDecision ( )
}
func contentFromEnvelope ( value binaryEnvelope ) ( [ ] byte , error ) {
content , err := base64 . StdEncoding . DecodeString ( value . ContentBase64 )
if err != nil {
return nil , fmt . Errorf ( "decode content_base64: %w" , err )
}
if digest := strings . TrimSpace ( value . ContentDigest ) ; digest != "" && digest != contentDigest ( content ) {
return nil , fmt . Errorf ( "content digest mismatch" )
}
return content , nil
}
2026-07-18 04:55:33 +00:00
func checkpointToPipelineFingerprints ( values [ ] Fingerprint ) [ ] pipeline . CheckpointFingerprint {
2026-07-08 03:02:50 +00:00
if len ( values ) == 0 {
return nil
}
out := make ( [ ] pipeline . CheckpointFingerprint , 0 , len ( values ) )
for _ , value := range values {
out = append ( out , pipeline . CheckpointFingerprint { Name : value . Name , Value : value . Value } )
}
return normalizeFingerprints ( out )
}
func fingerprintsEqual ( a [ ] pipeline . CheckpointFingerprint , b [ ] pipeline . CheckpointFingerprint ) bool {
a = normalizeFingerprints ( a )
b = normalizeFingerprints ( b )
if len ( a ) != len ( b ) {
return false
}
for i := range a {
if a [ i ] != b [ i ] {
return false
}
}
return true
}
func reusedDecision ( ) pipeline . CheckpointDecision {
2026-07-21 21:59:48 +00:00
return pipeline . CheckpointDecision { Reused : true , Category : "reused" , ReasonCode : "checkpoint_valid" , Reason : "checkpoint is valid" }
2026-07-08 03:02:50 +00:00
}
func invalidDecision ( format string , args ... any ) pipeline . CheckpointDecision {
2026-07-21 21:59:48 +00:00
reason := fmt . Sprintf ( format , args ... )
code := "checkpoint_invalid"
category := "executed"
switch {
case strings . Contains ( reason , "dependency fingerprints" ) :
code , category = "dependency_mismatch" , "dependency_invalidated"
case strings . Contains ( reason , "missing" ) :
code = "checkpoint_missing"
case strings . Contains ( reason , "schema" ) :
code = "workspace_schema_incompatible"
case strings . Contains ( reason , "codec" ) :
code = "artifact_codec_incompatible"
case strings . Contains ( reason , "content" ) :
code = "artifact_content_invalid"
case strings . Contains ( reason , "identity" ) :
code = "identity_mismatch"
}
safe := safeReasonText ( reason , code )
return pipeline . CheckpointDecision { Category : category , ReasonCode : code , Detail : safe , Reason : safe }
}
func safeReasonText ( reason , code string ) string {
lower := strings . ToLower ( reason )
switch {
case strings . Contains ( lower , "workspace schema" ) :
return "checkpoint workspace schema is incompatible"
case strings . Contains ( lower , "source checkpoint document" ) :
return "source checkpoint document is invalid"
case strings . Contains ( lower , "artifact codec identity" ) :
return "artifact codec identity is incomplete"
case strings . Contains ( lower , "base64" ) :
return "checkpoint content base64 is invalid"
case strings . Contains ( lower , "content digest" ) :
return "checkpoint content digest is invalid"
case strings . Contains ( lower , "output digest" ) :
return "checkpoint output digest does not match payload"
case strings . Contains ( lower , "identity" ) :
return "checkpoint identity does not match"
case strings . Contains ( lower , "dependency" ) :
return "checkpoint dependency fingerprints do not match"
case strings . Contains ( lower , "stage" ) :
return "checkpoint stage does not match"
case strings . Contains ( lower , "step" ) :
return "checkpoint step does not match"
case strings . Contains ( lower , "lane" ) :
return "checkpoint lane does not match"
case strings . Contains ( lower , "module" ) :
return "checkpoint module does not match"
case strings . Contains ( lower , "status" ) :
return "checkpoint status cannot be reused"
case strings . Contains ( lower , "payload" ) :
return "checkpoint artifact payload is invalid"
case strings . Contains ( lower , "decode" ) :
return "checkpoint artifact decode failed"
default :
return "checkpoint is not reusable (" + code + ")"
}
2026-07-08 03:02:50 +00:00
}