The claim
src/data/index.ts opens with a doctrine block that every seed record in the file is authored against:
The seed loader writes with { isSystem: true, skipTriggers: true }: hooks do NOT run over seed rows […] nothing recomputes derived fields for seed rows, so every seeded value of a hook-owned field MUST match what the hook would compute.
What actually happens
Boot log of a fresh install (pnpm dev, empty .objectstack/data), counted by hook + event:
35 opportunity_amount_rollup crm_opportunity_line_item afterInsert
35 opportunity_amount_rollup crm_opportunity_line_item afterUpdate
13 quote_total_rollup crm_quote_line_item afterInsert
13 quote_total_rollup crm_quote_line_item afterUpdate
and on a replay boot against the existing DB, the afterUpdate half fires again. Hooks demonstrably do run over seed writes on this runtime. (They are only visible at all because those two hooks currently throw — see the sibling issue about the update(id, doc) signature. Hooks that succeed leave no trace, so this went unnoticed.)
Why it matters
The doctrine is load-bearing in both directions and it is stated as fact:
- Its conclusion ("seeded values must equal what the hook computes") stays correct either way, and is worth keeping.
- Its premise is not, and the premise is what a future author reasons from. Someone who trusts "hooks don't run" will seed a value expecting it to survive verbatim — e.g. a hand-set
probability that a lifecycle hook then overwrites, or a status transition guard they assumed could not fire.
There is also a real behavioural question hiding behind it: if hooks run, seeding a row can trigger async parent writes, task creation, and account promotion at boot. That is arguably desirable (it is how the demo exercises its own automation) but it should be a decided, documented behaviour rather than a comment asserting the opposite.
Asks
- Establish what the loader actually passes (
skipTriggers may be version-dependent, or set only on one of the two write paths — note the boot also logs [Seeder] Inline seed exceeded 8000ms budget […] continuing in background, so there may be two paths with different options).
- Rewrite the doctrine block to match reality, keeping the "seeded value must equal the computed value" rule — which is the right rule under either behaviour.
- Consider a boot-time assertion or test that pins whichever behaviour is chosen, so this cannot silently flip on the next platform bump.
Found while implementing #591; filed unassigned per Prime Directive #10.
The claim
src/data/index.tsopens with a doctrine block that every seed record in the file is authored against:What actually happens
Boot log of a fresh install (
pnpm dev, empty.objectstack/data), counted by hook + event:and on a replay boot against the existing DB, the
afterUpdatehalf fires again. Hooks demonstrably do run over seed writes on this runtime. (They are only visible at all because those two hooks currently throw — see the sibling issue about theupdate(id, doc)signature. Hooks that succeed leave no trace, so this went unnoticed.)Why it matters
The doctrine is load-bearing in both directions and it is stated as fact:
probabilitythat a lifecycle hook then overwrites, or astatustransition guard they assumed could not fire.There is also a real behavioural question hiding behind it: if hooks run, seeding a row can trigger async parent writes, task creation, and account promotion at boot. That is arguably desirable (it is how the demo exercises its own automation) but it should be a decided, documented behaviour rather than a comment asserting the opposite.
Asks
skipTriggersmay be version-dependent, or set only on one of the two write paths — note the boot also logs[Seeder] Inline seed exceeded 8000ms budget […] continuing in background, so there may be two paths with different options).Found while implementing #591; filed unassigned per Prime Directive #10.