{"record":{"id":"7c2da53fd7f104a1","repo":"JuliusBrussee/caveman","slug":"session-value-artifact-training-lineage-invalid","errorCode":null,"errorMessage":"session-value artifact training lineage invalid","messagePattern":"session-value artifact training lineage invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/routing/session_value.go","lineNumber":178,"sourceCode":"\treturn artifact, nil\n}\n\nfunc ValidateSessionValueArtifact(artifact SessionValuePolicyArtifact, organizationID, projectID, candidatePoolHash string, now time.Time) error {\n\tif artifact.Schema != SessionValueArtifactSchema || artifact.StateSchemaVersion != SessionValueStateSchema {\n\t\treturn errors.New(\"session-value artifact schema mismatch\")\n\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\")","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/proxy/routing/session_value.go#L160-L196","documentation":"Training-lineage fields prove what data and contracts produced the artifact: TrainingManifestHash must be a 'sha256:<64 lowercase hex>' reference to the training manifest, and both TrainingExtractor (training_extractor_version) and OutcomeContractVersion must be non-empty. Missing lineage makes the policy unauditable, so validation rejects it.","triggerScenarios":"The artifact generator omitted the manifest hash or stored a bare 64-char digest without the 'sha256:' prefix (or with wrong length/case); training_extractor_version or outcome_contract_version left empty or renamed in the JSON so they unmarshal as empty strings.","commonSituations":"A trainer refactor dropping lineage fields; a manual or offline training run that skipped the manifest-hashing step; JSON field renames (training_extractor vs training_extractor_version) during schema migration.","solutions":["Stamp all three fields during training: the manifest digest as 'sha256:' + 64 lowercase hex chars, plus non-empty extractor and outcome-contract versions.","Run SealSessionValueArtifact after filling lineage so the artifact hash covers the lineage fields.","Add a trainer-side assertion that lineage fields are non-empty and correctly formatted before sealing."],"exampleFix":"// before\n{\n  \"training_manifest_hash\": \"9f2a...\",          // missing 'sha256:' prefix\n  \"training_extractor_version\": \"\",\n  \"outcome_contract_version\": \"\"\n}\n\n// after\n{\n  \"training_manifest_hash\": \"sha256:9f2a...<64 hex total>\",\n  \"training_extractor_version\": \"feat-ext-v3\",\n  \"outcome_contract_version\": \"outcome-v2\"\n}","handlingStrategy":"validation","validationCode":"// Before validation: check lineage format.\nvar sha256Ref = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)\nif !sha256Ref.MatchString(artifact.TrainingManifestHash) ||\n\tstrings.TrimSpace(artifact.TrainingExtractor) == \"\" ||\n\tstrings.TrimSpace(artifact.OutcomeContractVersion) == \"\" {\n\treturn errors.New(\"artifact lineage incomplete: need sha256:<64 hex> manifest hash plus extractor and outcome-contract versions\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always hash the training manifest during training and store it as 'sha256:' + 64 lowercase hex.","Make extractor and outcome-contract versions mandatory trainer outputs, failing the training job when unset.","Seal the artifact only after lineage is complete so the ArtifactHash covers it."],"tags":["routing","ml","lineage","hash","auditability"],"backgroundTag":"training-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"}