From 54a442c975f426297a2d60ce8ea1cb63f132c4f7 Mon Sep 17 00:00:00 2001 From: os-sam Date: Mon, 7 Sep 2026 17:50:50 +0000 Subject: [PATCH 1/2] feat(data): give every hired application its accepted offer; seed docs read 818 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG --- AGENTS.md | 4 +-- CONTRIBUTING.md | 2 +- DESIGN.md | 2 +- README.md | 10 +++--- docs/screenshots/README.md | 3 ++ src/data/demo-en/offer.seed.ts | 2 +- src/data/demo-zh/offer.seed.ts | 2 +- src/data/index.ts | 2 +- src/data/shared/build.ts | 10 ++++-- src/data/shared/pipeline.ts | 59 ++++++++++++++++++++++++---------- 10 files changed, 64 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2b3e2f2..05507c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ Paste the three green tails into the PR body. ## Booting with the demo seed — read before you count rows -The demo seed (`src/data/`, 801 rows incl. the 7 logins in `README.md`) is scoped `env: ['dev', 'test']` +The demo seed (`src/data/`, 818 rows incl. the 7 logins in `README.md`) is scoped `env: ['dev', 'test']` and loads **only when the boot's `NODE_ENV` resolves to development or test**. The CLI pins it for you: ```bash @@ -43,7 +43,7 @@ have checked the boot log for these lines**: | State | What the log says | Rows | |:--|:--|:--| -| seeded | `INFO [ats] demo seed enabled: NODE_ENV=development …` then `[Seeder] Seed loading complete {"inserted":801,…,"errored":0}` | 80 `ats_candidate`, 7 sign-ins work | +| seeded | `INFO [ats] demo seed enabled: NODE_ENV=development …` then `[Seeder] Seed loading complete {"inserted":818,…,"errored":0}` | 80 `ats_candidate`, 7 sign-ins work | | skipped | `WARN [ats] demo seed skipped: NODE_ENV=production …` (replayed under the banner at the default log level) and, at `--log-level info`, `[SeedLoader] Environment 'prod': skipped 16 dataset(s) …` | 0 everywhere, every persona answers `Invalid email or password` | If you exported `NODE_ENV=production` in your shell, `objectstack dev` keeps it and you get the skipped diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00a5fbc..b935d7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ gap; finding one is the finding. **Measure, do not assert.** This repository's review culture is that a claim in a PR body comes with the command that produced it and the number it produced. "It works" is not a sentence in a PR here; "the -Quillstone admin reads 5 jobs / 27 applications / 2 offers and 0 of Harborline's" is. Several cards have +Quillstone admin reads 5 jobs / 27 applications / 3 offers and 0 of Harborline's" is. Several cards have been overturned by their own developer measuring the premise — that is the standard, not the exception. ## Rules that are easy to break diff --git a/DESIGN.md b/DESIGN.md index 283af56..0edd682 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -315,7 +315,7 @@ record.id in current_user.applicant_candidate_ids 本机构申请人 | ats_candidate | 80 | 带头像;经验与技能有梯度 | | ats_application | 200 | 按漏斗铺:applied 88 · screening 46 · interview 28 · offer 14 · hired 9 · rejected 15 | | ats_interview | 40 | **未来两周内**有排期 | -| ats_offer | 14 | 3 条 `pending_approval` | +| ats_offer | 23 | 14 条挂在 offer 阶段的投递上(3 条 `pending_approval`)· 9 条 `accepted`,每个 hired 投递恰一条(#53) | | ats_inquiry | 8 | 全部 `new`;5 位无候选人行的投递者、3 位已入库候选人(转换后挂到既有行);2 条 Quillstone、2 条 Harborline | | ats_skill / ats_credential_type | 60 / 15 | 字典先行 | diff --git a/README.md b/README.md index dc12b45..27365f6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ pnpm dev # REST + Console on http://localhost:3000/_console/ — sign i ``` There is **no hosted demo yet** — nothing is deployed, so there is no link to give you; the three commands -above are the way to try it, and they boot the 809-row demo seed (the fresh-clone timing is measured in +above are the way to try it, and they boot the 818-row demo seed (the fresh-clone timing is measured in the card-14 pull request, not asserted here). What you get is one app with three audiences, each seeing exactly the navigation group its position unlocks: @@ -85,9 +85,9 @@ or `NODE_ENV=production` none of these rows is created and every sign-in below a | Boot | `NODE_ENV` the CLI pins | Demo rows | These logins | |---|---|---|---| -| `pnpm dev` · `objectstack dev` | `development` (when unset) | 809 seeded | work | +| `pnpm dev` · `objectstack dev` | `development` (when unset) | 818 seeded | work | | `objectstack start` · `objectstack serve` | `production` (when unset) | none | do not exist | -| `NODE_ENV=development objectstack start` | as exported | 809 seeded | work — deliberate opt-in | +| `NODE_ENV=development objectstack start` | as exported | 818 seeded | work — deliberate opt-in | | Sign in as | Password | Who | Sees | |---|---|---|---| @@ -99,8 +99,8 @@ or `NODE_ENV=production` none of these rows is created and every sign-in below a | `admin@harborline.example` | `demo1234` | Employer administrator, Harborline | **Hiring** group | | `candidate01@mail.example` | `demo1234` | Job seeker | **Job Seeker** group | -Employer isolation works: signed in as Quillstone you see 5 jobs, 27 applications, 2 offers, -3 team members and 2 inquiries; as Harborline, 5 / 31 / 2 / 3 / 2 — and neither sees a single row of +Employer isolation works: signed in as Quillstone you see 5 jobs, 27 applications, 3 offers, +3 team members and 2 inquiries; as Harborline, 5 / 31 / 4 / 3 / 2 — and neither sees a single row of the other's. ### The public application form diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md index f009017..3070234 100644 --- a/docs/screenshots/README.md +++ b/docs/screenshots/README.md @@ -8,6 +8,9 @@ OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai npx objectstack dev --fresh --database **every shot on the memory driver**, after the boot log's `[Seeder] Seed loading complete {"inserted":809,…,"errored":0}`, in a headless Chromium at 1440 × 900. +The seed has since grown to 818 rows — #53 added one `accepted` `ats_offer` per hired application, nothing +else — so a boot today logs `{"inserted":818,…}`; no screen below reads `ats_offer`, and the stage counts the +shots show (88 / 46 / 28 / 14 / 9 marketplace-wide, 10 / 8 / 5 / 2 / 1 for Quillstone) did not move. The driver matters: on the default (sqlite) driver the four tenancy-scoped objects return no rows to the platform personas ([#39](https://github.com/objectstack-ai/ats/issues/39)), so the Platform group's review queues photograph empty there. The demo logins are in the [README](../../README.md#demo-logins); the diff --git a/src/data/demo-en/offer.seed.ts b/src/data/demo-en/offer.seed.ts index cb6ef70..b416c8f 100644 --- a/src/data/demo-en/offer.seed.ts +++ b/src/data/demo-en/offer.seed.ts @@ -3,7 +3,7 @@ import { Offer } from '../../objects/offer.object.js'; import { buildOffers } from '../shared/build.js'; import { pack } from './pack.js'; -/** demo-en · 14 offers, 3 of them pending_approval. */ +/** demo-en · 23 offers: 14 on offer-stage applications (3 pending_approval) and one `accepted` per hired application (9). */ export const OfferSeed = defineSeed(Offer, { externalId: 'display_name', mode: 'upsert', diff --git a/src/data/demo-zh/offer.seed.ts b/src/data/demo-zh/offer.seed.ts index fa806b4..194a6e5 100644 --- a/src/data/demo-zh/offer.seed.ts +++ b/src/data/demo-zh/offer.seed.ts @@ -3,7 +3,7 @@ import { Offer } from '../../objects/offer.object.js'; import { buildOffers } from '../shared/build.js'; import { pack } from './pack.js'; -/** demo-zh · 14 offers, 3 of them pending_approval. */ +/** demo-zh · 23 offers: 14 on offer-stage applications (3 pending_approval) and one `accepted` per hired application (9). */ export const OfferSeed = defineSeed(Offer, { externalId: 'display_name', mode: 'upsert', diff --git a/src/data/index.ts b/src/data/index.ts index 357518d..29b4256 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -26,7 +26,7 @@ import { createDemoSeedGatePlugin, scopeToDemo } from './demo-seed-gate.js'; * ## Demo only — never production (#42) * * Whichever set is selected is scoped `env: ['dev', 'test']` by - * `scopeToDemo`, so the seed loader drops all of it — the 794 ATS/identity + * `scopeToDemo`, so the seed loader drops all of it — the 811 ATS/identity * rows AND the 7 `sys_account` logins whose passwords are in the README — on * any boot whose `NODE_ENV` resolves to production. `objectstack dev` sets * `NODE_ENV=development` and seeds; `objectstack start`/`serve` set diff --git a/src/data/shared/build.ts b/src/data/shared/build.ts index c548610..967e7ff 100644 --- a/src/data/shared/build.ts +++ b/src/data/shared/build.ts @@ -108,6 +108,8 @@ export { const daysAgo = (n: number) => cel`daysAgo(${n})`; const daysFromNow = (n: number) => cel`daysFromNow(${n})`; +/** A signed calendar offset: `n` days ahead, or `-n` days ago — for rows whose dates straddle seed time (accepted offers). */ +const dayOffset = (n: number) => (n < 0 ? daysAgo(-n) : daysFromNow(n)); /** A calendar day from seed time at a wall-clock hour: the calendar helpers land on UTC midnight, so add a duration. */ const dayAt = (day: number, hour: number, minute: number) => cel`daysFromNow(${day}) + duration(${minute > 0 ? `${hour}h${minute}m` : `${hour}h`})`; @@ -487,7 +489,9 @@ export function buildOffers(pack: LocalePack): TenantScopedSeedRecordOf Date: Mon, 7 Sep 2026 18:08:53 +0000 Subject: [PATCH 2/2] docs(dashboards): the seed now has an offer on every hired application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG --- src/dashboards/employer-hiring.dashboard.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dashboards/employer-hiring.dashboard.ts b/src/dashboards/employer-hiring.dashboard.ts index ef31d3e..40ebf79 100644 --- a/src/dashboards/employer-hiring.dashboard.ts +++ b/src/dashboards/employer-hiring.dashboard.ts @@ -18,8 +18,10 @@ import type { Dashboard } from '@objectstack/spec/ui'; * - the duration lives across two objects and needs a stored column * (`ats_application.days_to_offer`, stamped when the offer is written), * which is an object + hook change outside this card; - * - 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. + * - the demo seed does now carry one `accepted` offer per hired application + * (#53 — before it, all 14 offers sat on `offer`-stage applications and the + * tile would have read empty), so what keeps the tile out is the two + * reasons above, not the data. * The pipeline-by-stage bar takes its place so the surface shows the same * scoping at a glance; the tile returns with a card that adds the column. *