| 1 | package checkpoint |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "sort" |
| 9 | |
| 10 | fileenc "reasonix/internal/fileutil/encoding" |
| 11 | ) |
| 12 | |
| 13 | // load arbitrates legacy metadata, expired metadata, and v3 turn directories |
| 14 | // by checkpoint timestamp. Format priority only breaks an exact timestamp tie. |
| 15 | func (s *Store) load() { |
| 16 | type loadedCheckpoint struct { |
| 17 | checkpoint *Checkpoint |
| 18 | priority int |
| 19 | } |
| 20 | loaded := map[int]loadedCheckpoint{} |
| 21 | choose := func(c *Checkpoint, priority int) { |
| 22 | if c == nil { |
| 23 | return |
| 24 | } |
| 25 | current, ok := loaded[c.Turn] |
| 26 | if !ok || c.Time.After(current.checkpoint.Time) || |
| 27 | (c.Time.Equal(current.checkpoint.Time) && priority > current.priority) { |
| 28 | loaded[c.Turn] = loadedCheckpoint{checkpoint: c, priority: priority} |
| 29 | } |
| 30 | } |
| 31 | loadDir := func(dir string, expired bool) { |
| 32 | ents, err := os.ReadDir(dir) |
| 33 | if err != nil { |
| 34 | return |
| 35 | } |
| 36 | for _, e := range ents { |
| 37 | if e.IsDir() || filepath.Ext(e.Name()) != ".json" { |
| 38 | continue |
| 39 | } |
| 40 | var turnNum int |
| 41 | if _, err := fmt.Sscanf(e.Name(), "turn-%d.json", &turnNum); err != nil { |
| 42 | continue |
| 43 | } |
| 44 | b, err := fileenc.ReadFileUTF8(filepath.Join(dir, e.Name())) |
| 45 | if err != nil { |
| 46 | continue |
| 47 | } |
| 48 | var c Checkpoint |
| 49 | if json.Unmarshal(b, &c) != nil { |
| 50 | continue |
| 51 | } |
| 52 | if expired { |
| 53 | c.ExpiredFilePayload = true |
| 54 | for i := range c.Files { |
| 55 | c.Files[i].PayloadExpired = true |
| 56 | c.Files[i].BlobRef = "" |
| 57 | c.Files[i].Content = nil |
| 58 | } |
| 59 | } |
| 60 | // Mark v1 as legacy_unverified. |
| 61 | if c.SchemaVersion == 0 || c.SchemaVersion < SchemaV2 { |
| 62 | c.SchemaVersion = SchemaV1 |
| 63 | c.Legacy = true |
| 64 | c.Coverage = CoverageLegacy |
| 65 | hasLegacyGap := false |
| 66 | for _, g := range c.CoverageGaps { |
| 67 | if g.Reason == GapLegacyUnverified { |
| 68 | hasLegacyGap = true |
| 69 | break |
| 70 | } |
| 71 | } |
| 72 | if !hasLegacyGap { |
| 73 | c.CoverageGaps = append(c.CoverageGaps, CoverageGap{Reason: GapLegacyUnverified, Detail: "v1 checkpoint cannot verify later manual edits"}) |
| 74 | } |
| 75 | } |
| 76 | priority := 2 |
| 77 | if expired { |
| 78 | priority = 1 |
| 79 | } |
| 80 | choose(&c, priority) |
| 81 | } |
| 82 | } |
| 83 | loadDir(s.dir, false) |
| 84 | loadDir(s.expiredDir(), true) |
| 85 | // A v3 compatibility marker has the same timestamp as its turn directory, |
| 86 | // so v3 wins ties. A previous build that wrote a genuinely newer checkpoint |
| 87 | // with the same turn wins by timestamp instead of being hidden by load order. |
| 88 | for _, c := range s.loadV3Turns() { |
| 89 | choose(c, 3) |
| 90 | } |
| 91 | s.done = s.done[:0] |
| 92 | for _, item := range loaded { |
| 93 | s.done = append(s.done, item.checkpoint) |
| 94 | } |
| 95 | sort.Slice(s.done, func(i, j int) bool { return s.done[i].Turn < s.done[j].Turn }) |
| 96 | } |
| 97 |