{"record":{"id":"eefa7e27dc29f820","repo":"JuliusBrussee/caveman","slug":"session-value-artifact-rollback-lineage-invalid","errorCode":null,"errorMessage":"session-value artifact rollback lineage invalid","messagePattern":"session-value artifact rollback lineage invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/routing/session_value.go","lineNumber":184,"sourceCode":"\t}\n\tif strings.TrimSpace(artifact.OrganizationID) == \"\" || artifact.OrganizationID != strings.TrimSpace(organizationID) ||\n\t\tstrings.TrimSpace(artifact.ProjectID) == \"\" || artifact.ProjectID != strings.TrimSpace(projectID) {\n\t\treturn errors.New(\"session-value artifact tenant scope mismatch\")\n\t}\n\tif artifact.PolicyVersion <= 0 || artifact.RouterVersion != SessionValueRouterVersion || strings.TrimSpace(artifact.EstimatorVersion) == \"\" {\n\t\treturn errors.New(\"session-value artifact version identity invalid\")\n\t}\n\tif !validCompactPoolHash(artifact.CandidatePoolHash) || artifact.CandidatePoolHash != candidatePoolHash {\n\t\treturn errors.New(\"session-value artifact candidate pool mismatch\")\n\t}\n\tif !validSHA256Ref(artifact.TrainingManifestHash) || strings.TrimSpace(artifact.TrainingExtractor) == \"\" || strings.TrimSpace(artifact.OutcomeContractVersion) == \"\" {\n\t\treturn errors.New(\"session-value artifact training lineage invalid\")\n\t}\n\tif artifact.ValidFrom.IsZero() || artifact.ValidUntil.IsZero() || !artifact.ValidUntil.After(artifact.ValidFrom) || now.Before(artifact.ValidFrom) || !now.Before(artifact.ValidUntil) {\n\t\treturn errors.New(\"session-value artifact outside validity window\")\n\t}\n\tif artifact.RollbackParentHash != \"\" && (!validSHA256Ref(artifact.RollbackParentHash) || artifact.RollbackParentHash == artifact.ArtifactHash) {\n\t\treturn errors.New(\"session-value artifact rollback lineage invalid\")\n\t}\n\tif !finite(artifact.QualityUncertaintyZ) || artifact.QualityUncertaintyZ <= 0 || artifact.QualityUncertaintyZ > 5 ||\n\t\t!finite(artifact.MaxInversePropensity) || artifact.MaxInversePropensity < 1 || artifact.MaxInversePropensity > 100 {\n\t\treturn errors.New(\"session-value artifact confidence policy invalid\")\n\t}\n\tfeatureNames := SessionValueFeatureNames()\n\tif len(artifact.FeatureSpecs) != len(featureNames) || len(artifact.Actions) == 0 {\n\t\treturn errors.New(\"session-value artifact has no features or actions\")\n\t}\n\tfor i, spec := range artifact.FeatureSpecs {\n\t\tif spec.Name != featureNames[i] || (i > 0 && artifact.FeatureSpecs[i-1].Name >= spec.Name) {\n\t\t\treturn errors.New(\"session-value artifact feature vocabulary or order invalid\")\n\t\t}\n\t\tif spec.Name == \"turn_index\" && !spec.Required {\n\t\t\treturn errors.New(\"session-value artifact must require turn_index\")\n\t\t}\n\t\tif !finite(spec.Mean) || !finite(spec.Scale) || spec.Scale <= 0 || !finite(spec.Min) || !finite(spec.Max) || spec.Min < 0 || spec.Max < spec.Min {\n\t\t\treturn fmt.Errorf(\"session-value feature %q bounds invalid\", spec.Name)","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/proxy/routing/session_value.go#L166-L202","documentation":"RollbackParentHash is optional; when set it must be a valid 'sha256:<64 hex>' reference AND must differ from the artifact's own ArtifactHash — a policy cannot claim itself as its rollback parent. This preserves an auditable parent chain so rollbacks point at the artifact that was actually running before.","triggerScenarios":"Sealing re-computed the artifact hash after the parent field was copied from the same artifact, making parent == own hash; a truncated or bare-hex parent reference; copy-pasting the wrong hash field into rollback_parent_hash during manual rollback tooling.","commonSituations":"Automated rollback tooling that sets the parent after sealing (any field change invalidates the old hash); manual artifact surgery; hash-format drift between the tool that records parents and validSHA256Ref.","solutions":["Set RollbackParentHash to the sealed parent artifact's ArtifactHash before calling SealSessionValueArtifact, and re-seal if any field changed afterwards.","Leave RollbackParentHash empty when the artifact has no rollback parent — it is optional.","Validate the 'sha256:' + 64 hex format and parent != self at generation time, not just at load time."],"exampleFix":"// before\nartifact.RollbackParentHash = artifact.ArtifactHash // self-parent: rejected\n\n// after\nartifact.RollbackParentHash = parentArtifact.ArtifactHash // sealed previous artifact\nartifact, err := routing.SealSessionValueArtifact(artifact) // re-seal after any field change","handlingStrategy":"validation","validationCode":"// Before validation (after sealing): parent must be a valid ref and not the artifact itself.\nvar sha256Ref = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)\nif artifact.RollbackParentHash != \"\" &&\n\t(!sha256Ref.MatchString(artifact.RollbackParentHash) || artifact.RollbackParentHash == artifact.ArtifactHash) {\n\treturn errors.New(\"rollback parent must reference the sealed previous artifact (sha256:<64 hex>, not itself)\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Set RollbackParentHash from the sealed parent's ArtifactHash before sealing the child, and re-seal after any field change.","Leave the field empty when there is no parent rather than guessing a hash.","Record parent hashes in your deploy log so rollback tooling never copies the wrong value."],"tags":["routing","rollback","lineage","hash","artifact"],"backgroundTag":"rollback-lineage-invalid","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}