Skip to content

A duty's timezone has no source to resolve from — every instantiated duty is UTC #26

Description

@os-warren

Found while implementing catalog instantiation (#5). Not fixed there: there is nothing in that card's file surface to fix — the gap is that no producer exists.

The mapping the product wants

duly_duty.timezone is supposed to be "the user's zone if resolvable, else the org default, else UTC". Measured against @objectstack/spec@17.2.0, @objectstack/platform-objects@17.2.0 and @objectstack/plugin-security@17.2.0, the first two rungs are not reachable, so every duty created by duly_catalog_apply gets UTC.

1. There is no per-user zone

sys_user declares exactly:

name, email, email_verified, two_factor_enabled, role, banned, ban_reason,
ban_expires, failed_login_count, locked_until, password_changed_at,
phone_number, phone_number_verified, must_change_password, mfa_required_at,
last_login_at, last_login_ip, ai_access, image, manager_id,
primary_business_unit_id, source, id, created_at, updated_at

No timezone, no locale. (sys_user_preference is an open key/value bag — its documented examples are theme and locale. Writing a timezone key there would be an application-level convention that no platform surface reads or writes.)

2. The org default is real, but not reachable from an action handler

ExecutionContext.timezone is the resolved tenant zone (localization settings: platform default → global → tenant, ADR-0053 Phase 2). It is not propagated into an action handler's context: buildSession() builds ctx.session as { userId, organizationId, positions, roles, … } and carries no zone, and ActionHandlerContext declares record, params, user, session, engine — none of which exposes it.

So a handler that wants the tenant zone has nowhere honest to read it. A ctx.user.timezone ?? … chain would be a tolerant consumer standing in for a producer that does not exist — it would silently keep returning undefined and nobody would learn the zone was never resolved.

Why it matters here specifically

Periods and due dates are computed in the duty's own zone. A global customer rolling out a catalog to three sites gets all three on UTC, which is a plausible-looking wrong due date rather than a visible failure — the exact shape the engine's refusal-to-guess (see #24) is designed to avoid one layer down.

Not a duplicate of #24

#24 is about validating the string in the field: a typo'd IANA zone saves clean and throws at dispatch. This is about there being no source to populate that field from in the first place. Both are real; fixing either leaves the other.

Options

  • A. Add a timezone param to duly_catalog_apply. One zone per apply, chosen by the admin doing the rollout. Smallest change, uses the declared param surface, and matches how rollouts actually happen (a site at a time). Does not model a per-person zone.
  • B. Application-level user zone — a duly-owned object or a sys_user_preference convention, plus a resolver. Real per-person answer; a new store the platform does not know about, and a place for the value to go stale.
  • C. Ask the platform to carry ExecutionContext.timezone into the action-handler context, then read the tenant default. Fixes it for every app, not just this one; not a change this repo can make.
  • D. Leave it at UTC and document it as a deployment constraint.

Filing unassigned for triage — the choice is a product call.

Affects #20

The PM's assumption when dispatching #5 was that "the user's timezone if resolvable" had somewhere to resolve from. It does not. Whatever option is chosen, #20's deployment docs need to say what a duty's zone will actually be and what an operator has to do to get a non-UTC rollout right.

Where it is today

resolveDutyTimezone() in src/actions/catalog.handlers.ts returns UTC as a named function with this ladder written out, so there is exactly one place to add the rung when a source exists. It is pinned in test/catalog-instantiate.test.ts against duly_duty.timezone's own declared defaultValue, so the two cannot drift into two answers.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions