Skip to content

Seed doctrine says hooks never run over seed rows — they do, and the whole seed file is authored on that premise #617

Description

@os-zhuang

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

  1. 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).
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationmetadataDeclarative metadata — schema, security posture, UI surfacesprio:p1Next in line once P0s clear

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions