{"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/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/proposalrun/proposalrun.go#L336-L370","documentation":"VerifyChain re-walks a proposal's run rows sorted by seq and requires seq to start at 1 and increase by exactly 1. This error means the stored chain has a gap, a duplicate seq, or a seq that does not start at 1 — i.e. the audit hash chain's sequence integrity is broken (rows were deleted, inserted out of band, or written concurrently without proper sequencing).","triggerScenarios":"Calling VerifyChain(runs) where sorted Seqs are not 1..N: a row was hard-deleted, a duplicate seq exists, the first row's seq != 1, or rows from another proposal were mixed in.","commonSituations":"Manual DB cleanup deleting rows, failed/partial inserts leaving gaps, concurrent writers both claiming the next seq, migration scripts renumbering rows, copying rows between proposals.","solutions":["Identify the offending seq from the error and restore the missing/duplicate row from backup or the append API.","Re-append missing runs using the library's Append path so seq and prev_hash are recomputed correctly.","If rows were deleted intentionally, rebuild the chain from the authoritative event log rather than patching seqs by hand.","Add a DB constraint (unique seq per proposal) and use serialized/transactional appends to prevent recurrence."],"exampleFix":"// before\ndb.Exec(\"DELETE FROM proposal_runs WHERE id = $1\", id) // leaves a seq gap\n// after\n// soft-delete or append a compensating run so seq 1..N stays contiguous","handlingStrategy":"try-catch","validationCode":"func seqsContiguous(runs []proposalrun.Run) bool {\n\tif len(runs) == 0 { return true }\n\tseen := map[int64]bool{}\n\tfor _, r := range runs {\n\t\tif r.Seq < 1 || r.Seq > int64(len(runs)) || seen[r.Seq] { return false }\n\t\tseen[r.Seq] = true\n\t}\n\treturn len(seen) == len(runs)\n}","typeGuard":null,"tryCatchPattern":"if err := proposalrun.VerifyChain(runs); err != nil {\n\tif strings.Contains(err.Error(), \"out of order\") {\n\t\t// repair: re-append missing rows or restore from backup\n\t}\n\treturn fmt.Errorf(\"chain verify: %w\", err)\n}","preventionTips":["Never hard-delete proposal run rows; use compensating appends","Enforce UNIQUE(proposal_id, seq) in the schema","Append rows inside a transaction with serialized sequencing"],"tags":["audit-chain","integrity","hash-chain","database"],"backgroundTag":"internal-invariant-violation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}