feat(data): an accepted offer on every hired application; seed-count docs read 818 - #64
Conversation
…s read 818 The state machine reaches `hired` only from `offer`, and F3 writes the offer row, so a hired application without an offer is a history the app cannot produce — yet all 14 seeded offers sat on offer-stage applications and none of the 9 hired ones had one. Add one `accepted` offer per hired application (9 rows, 14 -> 23) in the skeleton both locale packs render through scopeToDemo(); the accepted rows carry approved_by and date offsets relative to the hire date, which is why the offer builder now takes a signed offset. Sizing: 14 -> 23 rather than re-balancing inside 14, because every offer-stage application needs its offer too and moving applications would move the 88/46/28/14/9 funnel that the release screenshots show. Seed-count statements move 809 -> 818 (README x3, screenshots README) and the stale 801/794 in AGENTS.md and src/data/index.ts are corrected at the same time; DESIGN.md §06's ats_offer row, the seed comments and the CONTRIBUTING example follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
The employer-hiring dashboard's comment justified leaving out the "median days to offer" tile partly on the grounds that "the demo seed has no offer on any hired application (its 14 offers all sit on `offer`-stage applications), so the tile would read empty anyway". This PR makes that false. The tile stays out for the two reasons that still hold — no median measure in the semantic layer, and the duration needs a stored column — so the bullet is rewritten to say that, rather than left asserting something the same PR disproves. Comment only; no dashboard, dataset or widget behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
Review — verified independently, then extended by one commitRe-measured from a clean worktree at The core claim, from the artifact I built myself
The invariant is now two-sided and both sides hold: every hired application has an accepted offer, and no accepted offer sits anywhere else. The locale packs are not asserted-equal, they were both built
Dates — checked for coherence, not just for presenceAn
So the negative offsets are a story the state machine can produce, not an artifact of arithmetic. Runtime, on a boot of my own (memory driver, port 4501)
Through REST, per persona (
Quillstone 5 / 27 / 3 / 3 / 2 and Harborline 5 / 31 / 4 / 3 / 2 — the README isolation sentence this PR rewrote is true as written. Each accepted offer's The hired ⟷ offer join re-run through RLS rather than over the artifact gives the same 9 of 9, with 0 unresolved lookups. The screenshots claim — checked at the source, not by eye
One thing the PR left false — fixed in
|
Closes #53
Closes #61
The state machine reaches
hiredonly fromoffer(DESIGN.md §02) and F3 writes the offer row, so a hired application without an offer is a history the app cannot produce — yet onorigin/main0ad34d7all 14 seededats_offerrows sit onoffer-stage applications and none of the 9 hired ones has one. This PR gives each hired application oneacceptedoffer, in both locale packs throughscopeToDemo(), and moves every seed-count statement to what a boot now logs (818). #61 is folded in:AGENTS.mdand thesrc/data/index.tscomment still said 801 / 794.Sizing decision: 14 → 23, not a re-balance
Every
offer-stage application needs its offer for the same state-machine reason the hired ones do (F3 produced it), so the 14 existing rows cannot be redistributed; a re-balance inside 14 would mean moving applications between stages, which moves the 88 / 46 / 28 / 14 / 9 funnel that the release screenshots anddocs/screenshots/README.mdshow. Adding 9 rows keeps every application count where it was — verified below, not assumed.What changed
src/data/shared/pipeline.tsOfferStatusgainsaccepted; 9acceptedrows on a000 a053 a056 a060 a099 a100 a121 a163 a164 (the ninehiredapplications), table kept sorted by application index; the header invariant rewritten to what is now true (23 · 14 on offer-stage + one accepted per hired)src/data/shared/build.tsbuildOffers: accepted rows carryapproved_by(they passed approval);start_date/expires_atgo through a signeddayOffset(daysFromNow(n)ordaysAgo(-n)) because an offer accepted 33 days ago has a start date and an expiry already in the pastsrc/data/demo-en/offer.seed.ts·src/data/demo-zh/offer.seed.tssrc/data/index.tsAGENTS.mdREADME.md5 / 31 / 2 / 3 / 2→5 / 31 / 4 / 3 / 2DESIGN.md§06ats_offerrow 14 → 23 (14 on offer-stage applications, 3 pending_approval · 9 accepted, one per hired application) — §06 only, §01–§03 untoucheddocs/screenshots/README.mdats_offer; the photographed stage counts did not moveCONTRIBUTING.mdNot touched (⛔ per the card):
src/security/,src/objects/,src/hooks/,src/flows/,src/views/,src/apps/,src/dashboards/,src/datasets/. No new label, option or field, so--i18n-stricthad nothing new to check. No dependency change.Gates —
pnpm validate && pnpm lint && pnpm typecheckon54a442cRun through
os-verify-lock.sh(exit codes captured before any pipe; the lock prints the verdict line):CI (
.github/workflows/ci.yml) runs the same three on this PR.Artifact join —
npx objectstack build,dist/objectstack.jsonThe reproduce one-liner from #53, plus the hired ⟷ offer join, before and after:
0ad34d7(origin/main)54a442c(this PR)Counter(stage of offer.application)Counter({'offer': 14})Counter({'offer': 14, 'hired': 9})ats_offerrows · by statusofferacceptedon ahiredapplication: true · every non-accepted on anofferapplication: trueats_applicationby stageRuntime read-back — memory driver, port 4403
OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai npx objectstack dev --fresh --database-driver memory -p 4403 --log-level info, counted only after the seeder line:Per persona,
GET /api/v1/data/OBJECT?$top=1&$count=true(total) afterPOST /api/v1/auth/sign-in/email:admin@platform.exampleadmin@quillstone.exampleadmin@harborline.exampleThe new rows as the employer persona reads them (
GET /api/v1/data/ats_offer?$filter={"status":"accepted"}), CEL dates resolved at seed time:Platform personas read 23 here because this is the memory driver; on sqlite
ats_offerreads 0 to them (#39, expected, not touched).Employer dashboard (
ats_employer_hiring) before / after — unchanged, as it must beNone of its four widgets reads
ats_offer(job, application, interview datasets only), so the numbers cannot move; measured anyway, both employer personas,POST /api/v1/analytics/dataset/querywith the widget's own filter asselection.runtimeFilter, cross-checked against a REST$filtercount (compiled SQL in the probe output shows the filter and the row-level scope both applied):REST cross-check column agreed with every analytics number in both runs. (Measurement note for whoever probes this next: a filter placed at
body.filter,selection.filter,filtersorwhereis accepted with HTTP 200 and silently ignored — the compiled SQL carries only the row-level scope, and Quillstone'sopen_jobsreads 5; the key the analytics service reads isselection.runtimeFilter. The Console uses that key, which is why the screenshots show 3 / 18 / 6.)Doc consistency — every seed-count statement now agrees
demo-seed-gate.ts, computed)README.mdline 18 / boot table ×2README.mdisolation sentenceAGENTS.md×2 (#61)src/data/index.ts(#61)DESIGN.md§06ats_offersrc/data/shared/pipeline.tsheader invariantdocs/screenshots/README.mdCONTRIBUTING.mdexample claimScreenshots — still valid, by count
Stage counts did not move: 88 / 46 / 28 / 14 / 9 marketplace-wide (artifact and the funnel dataset both), Quillstone 10 / 8 / 5 / 2 / 1 (+1 rejected) on the kanban and the Hiring Overview bar. Platform Overview reads employers / jobs / applications / candidates — none changed. The interview calendar reads
ats_interview(40, unchanged); Find Jobs readsats_job(22 published, unchanged). No shot readsats_offer, so none was reshot.Left as found, on purpose
src/dashboards/employer-hiring.dashboard.ts— its comment's third bullet ("the demo seed has no offer on any hired application (its 14 offers all sit on offer-stage applications)") is now false, butsrc/dashboards/is on this card's ⛔ list and Time-to-offer on the employer dashboard: stamp the duration onats_application, seed offers for hired applications, and correct §04's wording #55 — which builds that very tile and lists "Demo seed: noats_offerrow belongs to a hired application — all 14 offers sit onoffer-stage applications, so "days to offer for hired applications" is unmeasurable on the seed #53's seed fix" inside its own scope — will rewrite the block. After this merges, Time-to-offer on the employer dashboard: stamp the duration onats_application, seed offers for hired applications, and correct §04's wording #55's scope bullet on seeding offers is already done and its body item 4 is stale; flagged in the dev-report rather than edited here.docs/evidence/issue-8/*anddocs/backlog/10-seed-data.mdstill say 14 offers / 809 rows: dated measurement records and the original card, not living statements.offerwith adeclinedoffer — a legitimate transient (employer deciding whether to counter), pre-existing, not a state the machine forbids; noted, not changed.🤖 Generated with Claude Code
https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
Generated by Claude Code