Skip to content

Commit 91ba3c9

Browse files
os-warrenclaude
andcommitted
Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than five empty grids — a three-level business-unit tree, thirteen people, a twenty-item role catalog across three position codes, thirty-one duties and six months of dispatched history. History is produced by the dispatcher's own planner (`planDispatch`) rather than by a second period walk, so every period key is the engine's spelling by construction and "standing duties hold zero tasks" is structurally impossible to violate rather than merely absent from the fixture. `last_update_at` is written by a second `mode: 'update'` seed pass, per #32 / PR #64 — an insert can never carry it, and without that pass the "Not moving" view is empty while the seed reports success. Fixes #7 Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 45ab6a0 commit 91ba3c9

12 files changed

Lines changed: 2162 additions & 1 deletion

‎src/data/assignment.seed.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineSeed } from '@objectstack/spec/data';
4+
5+
import { Assignment } from '../objects/assignment.object.js';
6+
7+
import { ASSIGNMENTS } from './demo-assignments.js';
8+
9+
/**
10+
* Two assignments: one fanned out to four people with mixed completion, one
11+
* with `needs_collection` ticked.
12+
*
13+
* `task_count` is NOT seeded and must not be — it is an ADR-0021 summary the
14+
* platform computes over the children on read. Writing it would be a second
15+
* writer for a number the server owns, and it would be wrong the moment
16+
* anybody closed a task.
17+
*
18+
* `assignees` is `multiple: true`, so it is seeded as an ARRAY of natural
19+
* keys, one per element — a lone string is accepted as one-element shorthand,
20+
* which is not what these need.
21+
*/
22+
export const assignmentSeed = defineSeed(Assignment, {
23+
externalId: 'subject',
24+
mode: 'upsert',
25+
records: ASSIGNMENTS.map((assignment) => ({
26+
subject: assignment.subject,
27+
description: assignment.description,
28+
assigner: assignment.assigner,
29+
assignees: [...assignment.assignees],
30+
due_date: assignment.dueDate,
31+
// `dispatched`, not `draft`: the tasks exist, so the assignment that owns
32+
// them has to say it went out. A `draft` assignment with four children
33+
// would be a state the flow can never produce.
34+
status: 'dispatched',
35+
needs_collection: assignment.needsCollection,
36+
})),
37+
});

‎src/data/catalog.seed.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineSeed } from '@objectstack/spec/data';
4+
5+
import { CatalogItem } from '../objects/catalog-item.object.js';
6+
7+
import { CATALOG_ITEMS, cadenceOf } from './demo-catalog.js';
8+
9+
/**
10+
* The role catalog — twenty duty templates across three position codes.
11+
*
12+
* This is the screen that decides whether an evaluator believes the product:
13+
* "these are the 26 things a plant compliance officer owes" is the artefact
14+
* customers already have, usually as a spreadsheet, and seeing it rendered as
15+
* a first-class object is the moment the app stops looking like a to-do list.
16+
* `regulation_ref` is what does that work — a catalog without it reads as a
17+
* checklist, and with it as an audit answer.
18+
*
19+
* ── Cadence is filtered by form, not by hand ─────────────────────────────
20+
* `cadenceOf` decides which of the five cadence fields a row may carry (#61).
21+
* A standing item carrying a frequency is not merely odd — `standing_no_frequency`
22+
* REFUSES it, and the refusal takes the item, every duty instantiated from it
23+
* and every task under those duties. The two standing items below therefore
24+
* carry no frequency, no anchor, no offset, no lead and no grace; the
25+
* conditional `defaultValue` expressions resolve all five to null.
26+
*/
27+
export const catalogSeed = defineSeed(CatalogItem, {
28+
externalId: 'name',
29+
// Idempotent on re-run: matched by name, updated in place, and skipped
30+
// outright when nothing about the item has changed.
31+
mode: 'upsert',
32+
records: CATALOG_ITEMS.map((item) => ({
33+
name: item.name,
34+
position_code: item.position,
35+
form: item.form,
36+
description: item.description,
37+
regulation_ref: item.reference,
38+
active: item.active ?? true,
39+
...cadenceOf(item),
40+
})),
41+
});

‎src/data/demo-assignments.ts‎

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { visibleFromFor } from '../functions/period.js';
4+
5+
import { ADMIN } from './demo-org.js';
6+
import { NOW, TODAY } from './demo-history.js';
7+
8+
/**
9+
* The two assignments, and the tasks their fan-out would have produced.
10+
*
11+
* ⚠️ **The fan-out tasks are seeded directly, and that is not a shortcut.**
12+
* `assignment.flow.ts` is a `record_change` flow, and booting this app prints
13+
* `record_change triggers are not bound`. The flow therefore does not fire — on
14+
* a seeded assignment or on one created by hand in the UI. Seeding an
15+
* assignment and waiting for its children would leave the Assignments screen
16+
* showing two rows with `task_count: 0` and nothing to open, which is exactly
17+
* the "renders an empty screen" failure this card exists to prevent.
18+
*
19+
* So the rows below are written to be **byte-identical to what
20+
* `assignment.flow.ts` would have created**, field for field: `subject` copied
21+
* from the assignment, `owner` the assignee, `business_unit` denormalised from
22+
* the owner, `assignment` the parent, `source: 'assigned'`, `visible_from`
23+
* equal to `due_date` (an assignment has no lead time to spread), `status:
24+
* 'open'` at creation — and NO `period_key`, because an assignment has no
25+
* period and the dispatch identity index does not apply to it. When the
26+
* trigger binding is fixed, the flow's own idempotency guard (it looks for an
27+
* existing task on `(assignment, owner)` before creating one) sees these rows
28+
* and creates nothing, so the seed and the flow do not fight.
29+
*
30+
* The statuses below are then moved on from `open` by hand, because "mixed
31+
* completion" is the thing an assignment is worth looking at for.
32+
*/
33+
34+
/** `TODAY` shifted forward by `days`, through the period engine's own civil-date shift. */
35+
const inDays = (days: number): string => visibleFromFor(TODAY, -days);
36+
37+
const DAY = 24 * 60 * 60 * 1000;
38+
const HOUR = 60 * 60 * 1000;
39+
const daysAgo = (days: number): string => new Date(NOW.getTime() - days * DAY + 3 * HOUR).toISOString();
40+
41+
export interface DemoAssignment {
42+
subject: string;
43+
description: string;
44+
assigner: string;
45+
assignees: readonly string[];
46+
dueDate: string;
47+
needsCollection: boolean;
48+
}
49+
50+
export const ASSIGNMENTS: readonly DemoAssignment[] = [
51+
{
52+
subject: 'Winter shutdown readiness check',
53+
description:
54+
'Before the shutdown window opens, confirm your area is ready: isolations listed, spares on site, contractors booked. One line per point — no report.',
55+
// Assigned BY the account an evaluator is logged in as, so "Sent by me" is
56+
// not an empty screen on first boot.
57+
assigner: ADMIN,
58+
assignees: ['Marek Dvorak', 'Sami Okonkwo', 'Yuki Tanabe', 'Rosa Delgado'],
59+
dueDate: inDays(21),
60+
// The assigner gets NO task of their own. That is the product rule: a
61+
// manager who hands out work does not inherit a to-do list from it.
62+
needsCollection: false,
63+
},
64+
{
65+
subject: 'Q3 supplier certificate sweep',
66+
description:
67+
'Pull the current certificate for every approved supplier you buy from and flag any that expired during the quarter.',
68+
assigner: 'Priya Raman',
69+
assignees: ['Rosa Delgado', 'Ibrahim Chaudhry'],
70+
dueDate: inDays(10),
71+
// The other half of the rule: ticking this — and only ticking this — is
72+
// what gives the assigner a follow-up task once everyone is in.
73+
needsCollection: true,
74+
},
75+
];
76+
77+
export interface DemoAdHocTask {
78+
subject: string;
79+
owner: string;
80+
/** `duly_assignment.subject`, resolved as a natural key. Null for a plain one-off. */
81+
assignment: string | null;
82+
/** `duly_duty.name`. Null for a task that came out of an assignment. */
83+
duty: string | null;
84+
source: 'catalog' | 'assigned' | 'self';
85+
status: 'open' | 'in_progress' | 'done';
86+
dueDate: string;
87+
visibleFrom: string;
88+
completedAt?: string;
89+
lastUpdateAt: string;
90+
note?: string;
91+
}
92+
93+
const readiness = ASSIGNMENTS[0]!;
94+
const sweep = ASSIGNMENTS[1]!;
95+
96+
/**
97+
* The seven tasks the two fan-outs own, plus the one-off duty's single task.
98+
*
99+
* Mixed completion on the first assignment is the whole demonstration: four
100+
* independent rows, four owners, four different states, and NOBODY maintaining
101+
* a "2 of 4 done" field — `duly_assignment.task_count` is an ADR-0021 summary
102+
* the platform computes on read.
103+
*/
104+
export const AD_HOC_TASKS: readonly DemoAdHocTask[] = [
105+
// ── Winter shutdown readiness check — four people, mixed ───────────────
106+
{
107+
subject: readiness.subject,
108+
owner: 'Marek Dvorak',
109+
assignment: readiness.subject,
110+
duty: null,
111+
source: 'assigned',
112+
status: 'done',
113+
dueDate: readiness.dueDate,
114+
visibleFrom: readiness.dueDate,
115+
completedAt: daysAgo(4),
116+
lastUpdateAt: daysAgo(4),
117+
note: 'Isolations listed and countersigned. Spares are on site bar the two long-lead seals.',
118+
},
119+
{
120+
subject: readiness.subject,
121+
owner: 'Sami Okonkwo',
122+
assignment: readiness.subject,
123+
duty: null,
124+
source: 'assigned',
125+
status: 'done',
126+
dueDate: readiness.dueDate,
127+
visibleFrom: readiness.dueDate,
128+
completedAt: daysAgo(2),
129+
lastUpdateAt: daysAgo(2),
130+
},
131+
{
132+
subject: readiness.subject,
133+
owner: 'Yuki Tanabe',
134+
assignment: readiness.subject,
135+
duty: null,
136+
source: 'assigned',
137+
status: 'in_progress',
138+
dueDate: readiness.dueDate,
139+
visibleFrom: readiness.dueDate,
140+
lastUpdateAt: daysAgo(1),
141+
note: 'Contractor slot still to be confirmed for the Line C isolation.',
142+
},
143+
{
144+
subject: readiness.subject,
145+
owner: 'Rosa Delgado',
146+
assignment: readiness.subject,
147+
duty: null,
148+
source: 'assigned',
149+
status: 'open',
150+
dueDate: readiness.dueDate,
151+
visibleFrom: readiness.dueDate,
152+
lastUpdateAt: daysAgo(6),
153+
},
154+
155+
// ── Q3 supplier certificate sweep — two people, plus the assigner ──────
156+
{
157+
subject: sweep.subject,
158+
owner: 'Rosa Delgado',
159+
assignment: sweep.subject,
160+
duty: null,
161+
source: 'assigned',
162+
status: 'in_progress',
163+
dueDate: sweep.dueDate,
164+
visibleFrom: sweep.dueDate,
165+
lastUpdateAt: daysAgo(3),
166+
},
167+
{
168+
subject: sweep.subject,
169+
owner: 'Ibrahim Chaudhry',
170+
assignment: sweep.subject,
171+
duty: null,
172+
source: 'assigned',
173+
status: 'open',
174+
dueDate: sweep.dueDate,
175+
visibleFrom: sweep.dueDate,
176+
lastUpdateAt: daysAgo(5),
177+
},
178+
{
179+
// The follow-up the assigner asked for by ticking `needs_collection`.
180+
// Same shape as an assignee's: one owner, one row, nothing shared.
181+
subject: sweep.subject,
182+
owner: sweep.assigner,
183+
assignment: sweep.subject,
184+
duty: null,
185+
source: 'assigned',
186+
status: 'open',
187+
dueDate: sweep.dueDate,
188+
visibleFrom: sweep.dueDate,
189+
lastUpdateAt: daysAgo(5),
190+
},
191+
192+
// ── The one-off duty's single task ─────────────────────────────────────
193+
{
194+
// `subject` is copied from the duty at dispatch, exactly as
195+
// `dispatch.plan.ts` does it, so renaming the duty never rewrites history.
196+
subject: 'Commissioning file handover — Riverside upgrade',
197+
owner: 'Owen Pryce',
198+
assignment: null,
199+
duty: 'Commissioning file handover — Riverside upgrade',
200+
source: 'catalog',
201+
status: 'in_progress',
202+
// A one-off carries a due date set directly rather than derived from a
203+
// period anchor — which is why #61 takes `due_anchor` / `due_offset_days` /
204+
// `lead_days` off the one-off form entirely. It has no `period_key` either.
205+
dueDate: inDays(12),
206+
visibleFrom: inDays(-5),
207+
lastUpdateAt: daysAgo(2),
208+
note: 'As-builts and test records in; waiting on the spares list from the supplier.',
209+
},
210+
];

0 commit comments

Comments
 (0)