Found while building card 13 (#8). Read from dist/objectstack.json at 8d5dcc2 (the seed is literal rows, so this is a property of the source, not of a boot):
ats_application: 200 rows, stage counts applied 88 · screening 46 · interview 28 · offer 14 · hired 9 · rejected 15 · withdrawn 0.
ats_offer: 14 rows. Joining each offer's application back to its application: 14 of 14 point at an offer-stage application, 0 point at a hired one. The skeleton's own invariant says so (src/data/shared/pipeline.ts: "offers: 14 · one per offer-stage application").
ats_offer.status: sent 5 · pending_approval 3 · approved 3 · draft 2 · declined 1 — no accepted.
Why it matters: the state machine only reaches hired from offer (DESIGN.md §02), and F3 (offer_approval) is what produces the offer row — so a hired application with no offer is a history the app itself cannot produce. Concretely, the employer dashboard's fourth tile in card 13 ("median days from applied_at to the offer's created_at for hired applications") has no rows to read on the demo seed, independently of how it is computed. The 9 hired applications also carry no accepted offer for the offer views to show.
What a fix looks like (not done in #8 — src/data/ is outside that card): give each of the 9 hired applications one ats_offer row with status: 'accepted' (and a start_date), keeping the total offers coherent with DESIGN.md §06 (14 → 23, or re-balance), and add the invariant to the skeleton header. Whether ats_offer needs a stored accepted_at/created_at for the duration measure is the separate decision the #8 report raises.
Reproduce: python3 -c "import json,collections; d=json.load(open('dist/objectstack.json')); s={x['object']:x['records'] for x in d['data']}; st={a['display_name']:a['stage'] for a in s['ats_application']}; print(collections.Counter(st[o['application']] for o in s['ats_offer']))" → Counter({'offer': 14}).
Generated by Claude Code
Found while building card 13 (#8). Read from
dist/objectstack.jsonat8d5dcc2(the seed is literal rows, so this is a property of the source, not of a boot):ats_application: 200 rows, stage counts applied 88 · screening 46 · interview 28 · offer 14 · hired 9 · rejected 15 · withdrawn 0.ats_offer: 14 rows. Joining each offer'sapplicationback to its application: 14 of 14 point at anoffer-stage application, 0 point at ahiredone. The skeleton's own invariant says so (src/data/shared/pipeline.ts: "offers: 14 · one per offer-stage application").ats_offer.status: sent 5 · pending_approval 3 · approved 3 · draft 2 · declined 1 — noaccepted.Why it matters: the state machine only reaches
hiredfromoffer(DESIGN.md §02), and F3 (offer_approval) is what produces the offer row — so a hired application with no offer is a history the app itself cannot produce. Concretely, the employer dashboard's fourth tile in card 13 ("median days fromapplied_atto the offer'screated_atfor hired applications") has no rows to read on the demo seed, independently of how it is computed. The 9 hired applications also carry noacceptedoffer for the offer views to show.What a fix looks like (not done in #8 —
src/data/is outside that card): give each of the 9 hired applications oneats_offerrow withstatus: 'accepted'(and astart_date), keeping the total offers coherent with DESIGN.md §06 (14 → 23, or re-balance), and add the invariant to the skeleton header. Whetherats_offerneeds a storedaccepted_at/created_atfor the duration measure is the separate decision the #8 report raises.Reproduce:
python3 -c "import json,collections; d=json.load(open('dist/objectstack.json')); s={x['object']:x['records'] for x in d['data']}; st={a['display_name']:a['stage'] for a in s['ats_application']}; print(collections.Counter(st[o['application']] for o in s['ats_offer']))"→Counter({'offer': 14}).Generated by Claude Code