{"record":{"id":"35ce25e5d9a56d3f","repo":"JuliusBrussee/caveman","slug":"proposalrun-seq-d-out-of-order-expected-d","errorCode":null,"errorMessage":"proposalrun: seq %d out of order (expected %d)","messagePattern":"proposalrun: seq (.+?) out of order \\(expected (.+?)\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"shared/platform/proposalrun/proposalrun.go","lineNumber":354,"sourceCode":"// VerifyChain re-walks a proposal's runs ordered by seq and asserts:\n//   - seq starts at 1 and increases by exactly 1 (no gaps, no dupes);\n//   - each row's prev_hash equals the previous row's row_hash (\"\" for seq 1);\n//   - each row's row_hash recomputes from its own stored fields (no tampering).\n//\n// It returns nil for an empty slice (a proposal with no runs yet is consistent).\nfunc VerifyChain(runs []Run) error {\n\tif len(runs) == 0 {\n\t\treturn nil\n\t}\n\tsorted := make([]Run, len(runs))\n\tcopy(sorted, runs)\n\tsort.Slice(sorted, func(i, j int) bool { return sorted[i].Seq < sorted[j].Seq })\n\n\tprevHash := \"\"\n\tfor i, r := range sorted {\n\t\twantSeq := int64(i + 1)\n\t\tif r.Seq != wantSeq {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d out of order (expected %d)\", r.Seq, wantSeq)\n\t\t}\n\t\tif r.PrevHash != prevHash {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d prev_hash does not link to the prior row\", r.Seq)\n\t\t}\n\t\tgot, err := RowHash(r.PrevHash, r.Seq, r.Action, r.Detail, r.CostUSD, r.CreatedAt)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d recompute: %w\", r.Seq, err)\n\t\t}\n\t\tif got != r.RowHash {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d row_hash mismatch (tampered)\", r.Seq)\n\t\t}\n\t\tprevHash = r.RowHash\n\t}\n\treturn nil\n}\n","sourceCodeStart":336,"sourceCodeEnd":370,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/proposalrun/proposalrun.go#L336-L370","documentation":"VerifyChain re-establishes the hash chain over a slice of Run rows. It sorts by Seq, then requires Seq to be exactly 1,2,3,... with no gaps or duplicates. This error means the sorted sequence is not contiguous from 1: a row is missing (deleted), Seq values start above 1 (rows written after a purge without resetting sequence), or duplicates exist so numbering cannot line up with position.","triggerScenarios":"Call VerifyChain(runs) where runs was loaded with a filter that dropped rows (e.g. WHERE action != 'x'), after a manual DELETE left a gap, after a partial table copy, or after concurrent writers assigned duplicate Seq values.","commonSituations":"Audit/verification job run against a filtered query instead of the full table; a bug in the INSERT ... SELECT that allocates Seq; restoring a subset of rows from backup; splitting the table across shards and verifying one shard in isolation.","solutions":["Load the runs without WHERE filters — the chain is only defined over the complete ordered set.","If rows were legitimately deleted, renumber Seq contiguously from 1 and recompute PrevHash/RowHash for every subsequent row (a full re-chain), or keep gaps and change the verifier contract.","Check for duplicate Seq with SELECT seq, count(*) ... GROUP BY seq HAVING count(*) > 1 and fix the writer that produced them."],"exampleFix":"// before\nruns, _ := store.ListRuns(ctx, orgID, func(r Run) bool { return r.Action != \"draft\" }) // filters break contiguity\nerr := proposalrun.VerifyChain(runs)\n\n// after\nruns, err := store.ListRuns(ctx, orgID) // full set, unfiltered\nif err != nil { return err }\nerr = proposalrun.VerifyChain(runs)","handlingStrategy":"validation","validationCode":"func seqsContiguous(runs []proposalrun.Run) bool {\n    seen := make(map[int64]bool, len(runs))\n    for _, r := range runs {\n        if seen[r.Seq] { return false } // duplicate\n        seen[r.Seq] = true\n    }\n    for i := int64(1); i <= int64(len(runs)); i++ {\n        if !seen[i] { return false } // gap\n    }\n    return true\n}\n// call before VerifyChain: if !seqsContiguous(runs) { /* the query filtered rows */ }","typeGuard":null,"tryCatchPattern":"if err := proposalrun.VerifyChain(runs); err != nil {\n    if strings.HasPrefix(err.Error(), \"proposalrun: seq\") && strings.Contains(err.Error(), \"out of order\") {\n        // check for a WHERE filter in the load query or duplicate seq in the table\n    }\n}","preventionTips":["Always load the full ordered set (no filters, no LIMIT) before verifying.","Enforce UNIQUE(org_id, seq) at the schema level so duplicates cannot exist.","Verify on a schedule (cron/every N appends) so gaps surface immediately after the write that caused them."],"tags":["audit","hash-chain","data-integrity"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}