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 {
2026-07-22 02:13:50 +00:00
return pipeline . SourceCheckpoint { } , decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonArtifactPayloadInvalid , "source checkpoint document is invalid" )
2026-07-08 03:02:50 +00:00
}
if strings . TrimSpace ( manifest . SourceID ) != "" && manifest . SourceID != doc . ID {
2026-07-22 02:13:50 +00:00
return pipeline . SourceCheckpoint { } , decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonArtifactPayloadInvalid , "source checkpoint identity does not match its payload" )
2026-07-08 03:02:50 +00:00
}
2026-07-18 04:55:33 +00:00
if ! fingerprintsEqual ( checkpointToPipelineFingerprints ( manifest . OutputDigests ) , digestFingerprints ( "source_document" , doc . Digest ) ) {
2026-07-22 02:13:50 +00:00
return pipeline . SourceCheckpoint { } , decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonArtifactDigestMismatch , "source checkpoint output digest does not match its payload" )
2026-07-08 03:02:50 +00:00
}
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-22 02:13:50 +00:00
return pipeline . ExtractCheckpoint { } , artifactDecision ( err , "extract checkpoint artifact is invalid" )
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-22 02:13:50 +00:00
return pipeline . ExtractCheckpoint { } , decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonArtifactDigestMismatch , "extract checkpoint output digest does not match its 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-22 02:13:50 +00:00
return pipeline . MergeCheckpoint { } , artifactDecision ( err , "merge checkpoint artifact is invalid" )
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-22 02:13:50 +00:00
return pipeline . MergeCheckpoint { } , decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonArtifactDigestMismatch , "merge checkpoint output digest does not match its 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-22 02:13:50 +00:00
return pipeline . NormalizeCheckpoint { } , artifactDecision ( err , "normalize checkpoint artifact is invalid" )
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-22 02:13:50 +00:00
return pipeline . NormalizeCheckpoint { } , decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonArtifactDigestMismatch , "normalize checkpoint output digest does not match its 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 ) == "" {
2026-07-22 02:13:50 +00:00
return nil , & artifactPayloadError { code : pipeline . CheckpointReasonArtifactCodecIncompatible , err : fmt . Errorf ( "artifact codec identity is incomplete" ) }
2026-07-17 07:33:49 +00:00
}
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-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonLoadingDisabled , "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 {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonPathInvalid , "checkpoint path is invalid" )
2026-07-08 03:02:50 +00:00
}
data , err := os . ReadFile ( target )
if err != nil {
if os . IsNotExist ( err ) {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonMissing , "checkpoint artifact is missing" )
2026-07-08 03:02:50 +00:00
}
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonReadFailed , "checkpoint artifact could not be read" )
2026-07-08 03:02:50 +00:00
}
if err := json . Unmarshal ( data , out ) ; err != nil {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonDecodeFailed , "checkpoint artifact could not be decoded" )
2026-07-08 03:02:50 +00:00
}
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 {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonWorkspaceSchemaIncompatible , "checkpoint workspace schema is incompatible" )
2026-07-17 05:32:11 +00:00
}
2026-07-21 21:59:48 +00:00
if manifest . WorkspaceSchemaVersion == WorkspaceSchemaVersionV2 {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonWorkspaceSchemaIncompatible , "checkpoint workspace schema is incompatible" )
2026-07-21 21:59:48 +00:00
}
2026-07-18 04:55:33 +00:00
if manifest . WorkspaceSchemaVersion != WorkspaceSchemaVersion {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonWorkspaceSchemaIncompatible , "checkpoint workspace schema is incompatible" )
2026-07-08 03:02:50 +00:00
}
if strings . TrimSpace ( l . identityDigest ) != "" && manifest . Metadata [ "checkpoint_identity_digest" ] != l . identityDigest {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonIdentityMismatch , "checkpoint identity does not match the current invocation" )
2026-07-08 03:02:50 +00:00
}
if manifest . Stage != stage {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonStageMismatch , "checkpoint stage does not match the requested stage" )
2026-07-08 03:02:50 +00:00
}
2026-07-21 21:59:48 +00:00
if strings . TrimSpace ( stepID ) != "" && manifest . StepID != stepID {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonStepMismatch , "checkpoint step does not match the requested step" )
2026-07-21 21:59:48 +00:00
}
2026-07-08 03:02:50 +00:00
if strings . TrimSpace ( laneID ) != "" && manifest . LaneID != laneID {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonLaneMismatch , "checkpoint lane does not match the requested lane" )
2026-07-08 03:02:50 +00:00
}
if strings . TrimSpace ( moduleKey ) != "" && manifest . ModuleKey != moduleKey {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonModuleMismatch , "checkpoint module does not match the requested module" )
2026-07-08 03:02:50 +00:00
}
statusOK := false
for _ , status := range statuses {
if manifest . Status == status {
statusOK = true
break
}
}
if ! statusOK {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , pipeline . CheckpointReasonStatusNotReusable , "checkpoint status cannot be reused" )
2026-07-08 03:02:50 +00:00
}
2026-07-18 04:55:33 +00:00
if ! fingerprintsEqual ( checkpointToPipelineFingerprints ( manifest . DependencyFingerprints ) , dependencies ) {
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionDependencyInvalidated , pipeline . CheckpointReasonDependencyMismatch , "checkpoint dependencies do not match" )
2026-07-08 03:02:50 +00:00
}
return reusedDecision ( )
}
func contentFromEnvelope ( value binaryEnvelope ) ( [ ] byte , error ) {
content , err := base64 . StdEncoding . DecodeString ( value . ContentBase64 )
if err != nil {
2026-07-22 02:13:50 +00:00
return nil , & artifactPayloadError { code : pipeline . CheckpointReasonArtifactPayloadInvalid , err : fmt . Errorf ( "decode content_base64: %w" , err ) }
2026-07-08 03:02:50 +00:00
}
if digest := strings . TrimSpace ( value . ContentDigest ) ; digest != "" && digest != contentDigest ( content ) {
2026-07-22 02:13:50 +00:00
return nil , & artifactPayloadError { code : pipeline . CheckpointReasonArtifactDigestMismatch , err : fmt . Errorf ( "content digest mismatch" ) }
2026-07-08 03:02:50 +00:00
}
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-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionReused , pipeline . CheckpointReasonReused , "checkpoint is reusable" )
2026-07-08 03:02:50 +00:00
}
2026-07-22 02:13:50 +00:00
func decision ( category pipeline . CheckpointDecisionCategory , code pipeline . CheckpointReasonCode , detail string ) pipeline . CheckpointDecision {
return pipeline . NewCheckpointDecision ( category , code , detail )
2026-07-21 21:59:48 +00:00
}
2026-07-22 02:13:50 +00:00
type artifactPayloadError struct {
code pipeline . CheckpointReasonCode
err error
}
func ( e * artifactPayloadError ) Error ( ) string { return e . err . Error ( ) }
func artifactDecision ( err error , detail string ) pipeline . CheckpointDecision {
code := pipeline . CheckpointReasonArtifactPayloadInvalid
if payloadErr , ok := err . ( * artifactPayloadError ) ; ok {
code = payloadErr . code
2026-07-21 21:59:48 +00:00
}
2026-07-22 02:13:50 +00:00
return decision ( pipeline . CheckpointDecisionExecuted , code , detail )
2026-07-08 03:02:50 +00:00
}