Rebase seeded time-series data to now, so the demo stops going quiet
Some checks failed
CI / test (push) Has been cancelled
CI / fixture (push) Has been cancelled

ShiftRecord is generated against now (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 applications were sixteen days stale. Nothing was broken —
the data had simply aged out of every window the product reports over, and it
gets worse every day nobody reseeds.

RebaseToNow moves a set of records so the newest sits at now, keeping every gap
exactly as authored. The SHAPE is what every reader of this data is looking at:
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 it. Scaling into a window
or scattering events across recent days would invent a rhythm nobody wrote.

Applied per entity — UserActivity, JobApplication, AIInterview — because each
anchors on its own newest record. One shared anchor would drag the quieter
entities by another entity's delta and invent relationships between them.
Reference data is untouched: a course's date is a fact about the course, not a
position in a window.

Rebasing rather than generating, so seed.json stays the single authored source,
still deterministic and still comparable byte-for-byte by the drift check. The
alternative — excluding these from the fixture the way ShiftRecord is — means a
second generator to keep in step with the frontend's copy.

The existing TestSeedPreservesSourceValues caught a real bug in the first
attempt, and its comment is why: "Applications carry updated_date in the
source, and the gap from created_date is what buildHires reads as
time-to-hire." I had shifted created_date alone, which turned five-day hires
into three-week ones. EVERY timestamp on a record now moves by the same delta,
and there is a test on that specifically.

That test now asserts the GAP rather than the absolute dates, because for a
rebased entity the dates are deliberately different — which is the one reason
it is supposed to allow. Its real subject was always the interval.

Verified against the database: before, activity was 23 days old with 0 events
in the last 7; after, all three entities are current, with 24 applications
spread across the last 30 days and 15 activity events inside 30.

Not fixed here: the Hiring activity chart on Control Center still renders
empty, and it was equally empty before this change. Its bucketing is correct —
replaying it in the browser against live data matched all 24 applications into
the right days — so the fault is further down in that component, not in the
data. Naming it rather than leaving it implied by a chart that still looks
wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
This commit is contained in:
2026-08-29 15:13:33 +05:30
parent 48ab9d1dad
commit c378bc00ed
4 changed files with 348 additions and 4 deletions

View File

@@ -127,6 +127,19 @@ func Load(path string) (*Fixture, error) {
return &f, nil
}
// rebasedEntities names the entities whose dates are moved to sit against now,
// and the field to move them by.
//
// ShiftRecord is absent because it is GENERATED against now rather than
// rebased (see shifts.go). Everything not listed here is reference data —
// courses, role categories, badges — where a date is a fact about the record
// rather than a position in a window, and moving it would be a lie.
var rebasedEntities = map[string]string{
"UserActivity": "created_date",
"JobApplication": "created_date",
"AIInterview": "created_date",
}
// New builds a seeder. `now` anchors the generated shift records.
func New(pool *pgxpool.Pool, fixture *Fixture, now time.Time) *Seeder {
return &Seeder{pool: pool, fixture: fixture, now: now}
@@ -158,6 +171,15 @@ func (s *Seeder) Run(ctx context.Context) (*Result, error) {
if entity == "ShiftRecord" {
records = BuildShifts(s.now)
}
// Time-series entities are authored on a fixed calendar and would
// otherwise age out of every window the product asks about — the
// activity tools, and the hiring chart on Control Center. See
// RebaseToNow. Each is anchored on its OWN newest record, so the gaps
// within an entity are preserved without inventing a relationship
// between entities.
if field, ok := rebasedEntities[entity]; ok {
records = RebaseToNow(records, field, s.now)
}
count, err := s.upsertEntity(ctx, tx, orgID, entity, records)
if err != nil {
return nil, fmt.Errorf("seed %s: %w", entity, err)