{"record":{"id":"69d2f605f35f20c1","repo":"JuliusBrussee/caveman","slug":"session-value-artifact-feature-vocabulary-or-order","errorCode":null,"errorMessage":"session-value artifact feature vocabulary or order invalid","messagePattern":"session-value artifact feature vocabulary or order invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/routing/session_value.go","lineNumber":196,"sourceCode":"\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)\n\t\t}\n\t}\n\tseenActions := map[string]struct{}{}\n\tartifactPool := make([]Candidate, 0, len(artifact.Actions))\n\tfor i, action := range artifact.Actions {\n\t\twantID := CandidateActionID(Candidate{Provider: action.Provider, Model: action.Model, Effort: action.Effort})\n\t\tif wantID == \"\" || action.ActionID != wantID || (i > 0 && artifact.Actions[i-1].ActionID >= action.ActionID) {\n\t\t\treturn errors.New(\"session-value artifact action identity or order invalid\")\n\t\t}\n\t\tif _, duplicate := seenActions[action.ActionID]; duplicate {\n\t\t\treturn errors.New(\"session-value artifact duplicate action\")\n\t\t}","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/proxy/routing/session_value.go#L178-L214","documentation":"FeatureSpecs must match SessionValueFeatureNames() name-for-name and index-for-index, in strictly ascending sorted order. The evaluator aligns model coefficients by position, so any renamed feature, missing entry, duplicate, or unsorted array breaks interpretation and is rejected.","triggerScenarios":"Trainer emitting features in training/discovery order instead of sorted order; adding or renaming a feature on one side (trainer or router) only; duplicate names appearing after merging two spec lists; locale-aware sorting in a generation script producing a different order than Go's sort.Strings.","commonSituations":"Vocabulary drift between trainer and router versions; hand-reordered JSON arrays; spec lists built from map iteration (random order in Go) without sorting.","solutions":["Sort FeatureSpecs by name with plain lexicographic ordering (matching sort.Strings) before sealing the artifact.","Regenerate the artifact whenever the vocabulary changes; never hand-edit the array order.","Assert spec.Name == SessionValueFeatureNames()[i] for every index in trainer tests."],"exampleFix":"// before\nspecs := specsFromMap(featureMap) // Go map iteration: random order\n\n// after\nnames := routing.SessionValueFeatureNames()\nsort.Slice(specs, func(i, j int) bool { return specs[i].Name < specs[j].Name })\nfor i := range specs {\n\tif specs[i].Name != names[i] {\n\t\treturn fmt.Errorf(\"feature %q not in vocabulary position %d\", specs[i].Name, i)\n\t}\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// featureSpecsAligned reports whether specs exactly match the router's sorted vocabulary.\nfunc featureSpecsAligned(specs []routing.SessionValueFeatureSpec) bool {\n\twanted := routing.SessionValueFeatureNames()\n\tif len(specs) != len(wanted) {\n\t\treturn false\n\t}\n\tfor i, spec := range specs {\n\t\tif spec.Name != wanted[i] {\n\t\t\treturn false\n\t\t}\n\t\tif i > 0 && specs[i-1].Name >= spec.Name { // strictly ascending\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}","tryCatchPattern":null,"preventionTips":["Always build FeatureSpecs by iterating SessionValueFeatureNames() in order — never from map iteration.","Sort specs with plain byte-wise string comparison; avoid locale-aware collation in generation scripts.","Assert alignment in trainer tests so vocabulary drift fails CI before shipping."],"tags":["routing","features","ordering","vocabulary","artifact"],"backgroundTag":"feature-vocabulary-mismatch","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}