package seeder import ( "strings" "time" ) // RebaseToNow moves a fixture's records so the most recent one sits at `now`, // keeping every gap between them exactly as authored. // // The problem this solves is that the demo goes quiet. ShiftRecord is generated // against now (see shifts.go); everything else stayed on the fixed calendar in // seed.js while the calendar moved on. Twenty-three days after that file was // written the activity agent truthfully reported zero events in the last seven // days, and the hiring chart on Control Center drew one bar in a thirty-day // window. Nothing was broken; the data had simply aged out of every window the // product asks about. // // Applied per entity rather than globally, because each is anchored on its own // newest record. Rebasing them together against one shared anchor would drag // the quieter entities forward or back by another entity's delta and invent // relationships between them that nobody authored. // // Rebasing rather than generating, deliberately. ShiftRecord is excluded from // seed.json because it is built fresh each run; doing the same here would mean // a second generator to keep in step with the frontend's copy, and a fixture // that no longer describes what a seeded workspace contains. Rebasing keeps // seed.json the single authored source — still deterministic, still comparable // byte-for-byte by the drift check — and moves the window instead of the data. // // The SHAPE is what matters to every reader of this data: three hires on one // day, a screening the day after, a quiet fortnight before it. Shifting the // whole set by one delta preserves all of that. Scaling it into a window, or // scattering events across recent days, would invent a rhythm nobody authored. // // EVERY timestamp on a record moves by the same delta, not just the anchor. // The gaps WITHIN a record are as load-bearing as the gaps between them: an // application's created_date and updated_date are what buildHires subtracts to // get time-to-hire, so shifting one and not the other invents a hire that took // three weeks or minus one. The seeder's own TestSeedPreservesSourceValues // caught exactly that, which is why it says what it says about the gap. // // Idempotent: the delta is recomputed from the fixture every run, so seeding // twice rebases the same authored dates twice rather than compounding. // Returns the records unchanged when there are none, or when none of them // carry a usable anchor. func RebaseToNow(records []map[string]any, field string, now time.Time) []map[string]any { if len(records) == 0 { return records } var newest time.Time for _, rec := range records { if at, ok := recordTime(rec, field); ok && at.After(newest) { newest = at } } if newest.IsZero() { // Nothing parseable to anchor on. Leaving the fixture alone is the // honest outcome: a wrong guess about these dates is worse than dates // that are visibly old. return records } delta := now.Sub(newest) out := make([]map[string]any, 0, len(records)) for _, rec := range records { if _, ok := recordTime(rec, field); !ok { out = append(out, rec) continue } // Copied, not mutated: the fixture is read once and seeded into // possibly several organizations, and rewriting it in place would make // the second one depend on the first. shifted := make(map[string]any, len(rec)) for k, v := range rec { shifted[k] = v // Every timestamp moves together. A field that is not a timestamp, // or a null one, is copied across untouched. if at, isTime := recordTime(rec, k); isTime { shifted[k] = at.Add(delta).UTC().Format(dateLayoutOf(rec[k])) } } out = append(out, shifted) } return out } // recordTime reads one timestamp field, whatever shape the fixture used. func recordTime(rec map[string]any, field string) (time.Time, bool) { raw, ok := rec[field] if !ok { return time.Time{}, false } switch v := raw.(type) { case time.Time: return v, true case string: for _, layout := range []string{time.RFC3339Nano, time.RFC3339, "2006-01-02"} { if at, err := time.Parse(layout, v); err == nil { return at, true } } } return time.Time{}, false } // dateLayoutOf keeps a rewritten timestamp in the shape the fixture used, so a // date-only field stays a date and a full timestamp keeps its precision. // Anything else the seeder reads back would differ from the source for reasons // that have nothing to do with the shift. func dateLayoutOf(raw any) string { if s, ok := raw.(string); ok { if len(s) == len("2006-01-02") { return "2006-01-02" } if strings.Contains(s, ".") { return "2006-01-02T15:04:05.000Z" } } return time.RFC3339 }