diff --git a/src/data/shared/build.ts b/src/data/shared/build.ts index 967e7ff..1e9cdd2 100644 --- a/src/data/shared/build.ts +++ b/src/data/shared/build.ts @@ -8,9 +8,25 @@ * package was built and two consecutive builds stay byte-identical. * * Denormalised fields are written EXPLICITLY here — `display_name` mirrors, - * `employer`, `employer_org`, `candidate_user` — because a seed row does not - * go through the stamp hooks (`skipTriggers`), and a seed that leaned on them - * would break silently the day a hook changed. + * `employer`, `employer_org`, `candidate_user` — so the seed states the facts + * it means rather than inheriting whatever a hook computes today, and a seed + * that leaned on them would break silently the day a hook changed. + * + * ⚠️ This block used to say a seed row "does not go through the stamp hooks + * (`skipTriggers`)". That is false, and the flag it cited is the reason it is + * false. `skipTriggers` is real and the seed loader does carry it, but the + * loader's own contract says what it suppresses: record-change AUTOMATION — + * autolaunched flow triggers — because a seed is end-state data, not a stream + * of user events. Its next sentence is the one that matters here: + * "Lifecycle HOOKS (derived/default fields, validation) still run." + * (`@objectstack/metadata-protocol` `src/seed-loader.ts`, the SEED_OPTIONS + * docblock.) So every stamp hook in `src/hooks/` DOES fire on these rows — + * which is exactly how #65 happened: an unconditional `last_activity_at` + * assignment overwrote the authored value on all 200 applications, and the + * comment that said hooks could not reach a seed row is what would have + * stopped someone looking. Seed writes also carry `seedReplay`, which skips + * the object's `state_machine` entry and transition checks — that is why an + * `ats_offer` can be seeded straight into its terminal `accepted` state (#53). * * The kernel's tenant column `organization_id` is written explicitly as well, * on the four objects that stay inside the Layer 0 tenant wall (DESIGN.md §03, diff --git a/src/flows/application-stage-notify.flow.ts b/src/flows/application-stage-notify.flow.ts index 733d516..69ffb17 100644 --- a/src/flows/application-stage-notify.flow.ts +++ b/src/flows/application-stage-notify.flow.ts @@ -15,8 +15,12 @@ import { defineFlow, cel } from '@objectstack/spec'; * * DESIGN.md §05 reads "refresh `last_activity_at`". That already happens on the * SAME write that changes the stage: `ApplicationStampHook` runs at - * `beforeUpdate` and stamps `last_activity_at = now()` on every application - * update, unconditionally (stamp.hook.ts, last line of the handler). By the + * `beforeUpdate` and stamps `last_activity_at = now()` on any update whose + * payload names a field of the application — which a stage change is. (It is + * NOT unconditional, as this paragraph claimed before #65: a payload of only + * bookkeeping columns — the ownership claim, a derived roll-up — deliberately + * does not move the activity clock, and an authored value always wins. See + * stamp.hook.ts, "What counts as activity on an application".) By the * time this `record-after-update` flow starts, the value is already the * timestamp of the stage change, and `record.last_activity_at` carries it. A * second `update_record` here would issue another write per stage change, run