Skip to content

Task lifecycle hook — completed_at and last_update_at stamping #3

Description

@os-warren

Two server-owned timestamps on duly_task. One of them is the most useful number in the product.

Files you own

  • src/hooks/task.hook.ts (new), added to dulyHooks in src/hooks/index.ts
  • test/task-hook.test.ts (new)

Hooks are read from defineStack({ hooks }) only — a *.hook.ts that is not in the barrel type-checks, reads as wired, and never runs. The barrel is already imported by the config; do not touch the config.

Behaviour

completed_at and last_update_at are both readonly: true, meaning a non-system caller's write is stripped. The hook is the one writer.

beforeInsert

  • last_update_at = now

beforeUpdate

  • transition into status = 'done' → stamp completed_at = now
  • transition out of done → clear completed_at to null
  • last_update_at = now only when something meaningful changed: status, note, skip_reason, or an attachment. See below.

The trap

last_update_at is the stagnation signal — the "Not moving" view is status in (open, in_progress) AND last_update_at < {14_days_ago}. If the hook stamps on every update, then any unrelated write — a bulk re-owner, a business-unit backfill, an import — silently resets the stagnation clock on the entire table and the signal goes quiet exactly when it matters.

Stamp on the fields a human touching the task would change. Do not stamp on system-owned or administrative field changes.

The other trap

duly_task has a validation rule completed_at_required_when_done. It is satisfied by the server: this hook stamps before validation runs, so a completion write carrying only { status: 'done' } passes. That rule exists as the assertion that the stamp happened — if this hook is ever unregistered, the write is refused loudly instead of committing a done task with no timestamp. Do not "fix" the rule; make the hook satisfy it.

Acceptance

  • { status: 'done' } alone commits, with completed_at set
  • reopening (done → in_progress) clears completed_at; the record then passes validation
  • a caller passing completed_at explicitly has it stripped and replaced
  • editing note advances last_update_at
  • changing only business_unit (or any bulk administrative write) does not advance last_update_at — assert this directly, it is the whole point
  • re-saving with no changes does not advance it either

Gates

pnpm validate && pnpm typecheck && pnpm test && pnpm build.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions