Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | 字典先行 |

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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 |
|---|---|---|---|
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions src/dashboards/employer-hiring.dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
2 changes: 1 addition & 1 deletion src/data/demo-en/offer.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/data/demo-zh/offer.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/data/shared/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`})`;
Expand Down Expand Up @@ -487,7 +489,9 @@ export function buildOffers(pack: LocalePack): TenantScopedSeedRecordOf<typeof O
const { index: employerIndex, row: e } = employerOf(a.job);
const name = applicationName(pack, a);
const salary = Math.round((job.salaryMin + job.salaryMax) / 2 / 10) * 10;
const approved = o.status === 'approved' || o.status === 'sent' || o.status === 'declined';
// Everything past internal approval carries the approver — an accepted
// offer was approved, sent and then accepted (DESIGN.md §02 state machine).
const approved = o.status === 'approved' || o.status === 'sent' || o.status === 'accepted' || o.status === 'declined';
return {
display_name: `Offer · ${name}`,
application: name,
Expand All @@ -497,10 +501,10 @@ export function buildOffers(pack: LocalePack): TenantScopedSeedRecordOf<typeof O
candidate_user: candidateUserId(a.candidate),
salary,
salary_period: job.salaryPeriod,
start_date: daysFromNow(o.startInDays),
start_date: dayOffset(o.startInDays),
status: o.status,
...(approved ? { approved_by: adminEmail(e.slug) } : {}),
expires_at: daysFromNow(o.expiresInDays),
expires_at: dayOffset(o.expiresInDays),
notes: pack.offerNote,
};
});
Expand Down
59 changes: 42 additions & 17 deletions src/data/shared/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
* hired 9 · rejected 15 · withdrawn 0 · (job, candidate) unique
* interviews: 40 · every row on an interview-stage application ·
* day 1–14 from seed time (12 applications carry a round 2)
* offers: 14 · one per offer-stage application · 3 pending_approval
* offers: 23 · one per offer-stage application (14: 2 draft ·
* 3 pending_approval · 3 approved · 5 sent · 1 declined) and
* one `accepted` per hired application (9) — `hired` is
* reachable only from `offer` (DESIGN.md §02) and F3 writes
* the offer row, so a hired application without its accepted
* offer would be a history the app cannot produce (#53)
* inquiries: 8 · all `new` · 5 from people with no candidate row, 3 from
* seeded candidates (converting one attaches to that row) ·
* every job published · 2 Quillstone · 2 Harborline
Expand Down Expand Up @@ -52,12 +57,19 @@ export interface InterviewRow {
mode: 'onsite' | 'video' | 'phone';
}

export type OfferStatus = 'draft' | 'pending_approval' | 'approved' | 'sent' | 'declined';
export type OfferStatus = 'draft' | 'pending_approval' | 'approved' | 'sent' | 'accepted' | 'declined';

export interface OfferRow {
/** Index into APPLICATIONS (an offer-stage row). */
/** Index into APPLICATIONS an offer-stage row, or a hired row when `status` is `accepted`. */
application: number;
status: OfferStatus;
/**
* Calendar days from seed time (`daysFromNow(n)`); negative = that many
* days ago (`daysAgo(-n)`). Only accepted offers go negative: the hire
* happened `activityDaysAgo` days ago, the start date follows it by two to
* four weeks and the expiry it beat sits a few days after the acceptance,
* so both may already be in the past for the longer-hired rows.
*/
startInDays: number;
expiresInDays: number;
}
Expand Down Expand Up @@ -433,18 +445,31 @@ export const INTERVIEWS: readonly InterviewRow[] = [
];

export const OFFERS: readonly OfferRow[] = [
{ application: 1, status: "pending_approval", startInDays: 21, expiresInDays: 7 },
{ application: 15, status: "sent", startInDays: 24, expiresInDays: 8 },
{ application: 30, status: "approved", startInDays: 27, expiresInDays: 9 },
{ application: 35, status: "draft", startInDays: 30, expiresInDays: 10 },
{ application: 62, status: "sent", startInDays: 33, expiresInDays: 11 },
{ application: 71, status: "pending_approval", startInDays: 36, expiresInDays: 12 },
{ application: 78, status: "sent", startInDays: 39, expiresInDays: 13 },
{ application: 87, status: "approved", startInDays: 42, expiresInDays: 14 },
{ application: 106, status: "declined", startInDays: 45, expiresInDays: 7 },
{ application: 123, status: "sent", startInDays: 48, expiresInDays: 8 },
{ application: 128, status: "pending_approval", startInDays: 51, expiresInDays: 9 },
{ application: 140, status: "approved", startInDays: 54, expiresInDays: 10 },
{ application: 180, status: "draft", startInDays: 57, expiresInDays: 11 },
{ application: 186, status: "sent", startInDays: 60, expiresInDays: 12 },
// Sorted by application index. `accepted` rows sit on the nine hired
// applications (a000 a053 a056 a060 a099 a100 a121 a163 a164); every other
// status sits on an offer-stage application. Offsets on the accepted rows
// are relative to that application's `activityDaysAgo` (the hire date).
{ application: 0, status: "accepted", startInDays: 11, expiresInDays: -5 }, // hired 10 days ago
{ application: 1, status: "pending_approval", startInDays: 21, expiresInDays: 7 },
{ application: 15, status: "sent", startInDays: 24, expiresInDays: 8 },
{ application: 30, status: "approved", startInDays: 27, expiresInDays: 9 },
{ application: 35, status: "draft", startInDays: 30, expiresInDays: 10 },
{ application: 53, status: "accepted", startInDays: -4, expiresInDays: -25 }, // hired 32 days ago
{ application: 56, status: "accepted", startInDays: -1, expiresInDays: -17 }, // hired 22 days ago
{ application: 60, status: "accepted", startInDays: 15, expiresInDays: -8 }, // hired 15 days ago
{ application: 62, status: "sent", startInDays: 33, expiresInDays: 11 },
{ application: 71, status: "pending_approval", startInDays: 36, expiresInDays: 12 },
{ application: 78, status: "sent", startInDays: 39, expiresInDays: 13 },
{ application: 87, status: "approved", startInDays: 42, expiresInDays: 14 },
{ application: 99, status: "accepted", startInDays: 18, expiresInDays: 1 }, // hired 6 days ago
{ application: 100, status: "accepted", startInDays: 18, expiresInDays: 4 }, // hired 3 days ago
{ application: 106, status: "declined", startInDays: 45, expiresInDays: 7 },
{ application: 121, status: "accepted", startInDays: -19, expiresInDays: -29 }, // hired 33 days ago
{ application: 123, status: "sent", startInDays: 48, expiresInDays: 8 },
{ application: 128, status: "pending_approval", startInDays: 51, expiresInDays: 9 },
{ application: 140, status: "approved", startInDays: 54, expiresInDays: 10 },
{ application: 163, status: "accepted", startInDays: 2, expiresInDays: -20 }, // hired 26 days ago
{ application: 164, status: "accepted", startInDays: 22, expiresInDays: -1 }, // hired 6 days ago
{ application: 180, status: "draft", startInDays: 57, expiresInDays: 11 },
{ application: 186, status: "sent", startInDays: 60, expiresInDays: 12 },
];
Loading