Skip to content

feat!: one activity per work item, without an event type - #103

Open
claude-for-timatic[bot] wants to merge 4 commits into
mainfrom
feat/41-code-review-activity
Open

claude-for-timatic[bot] wants to merge 4 commits into
mainfrom
feat/41-code-review-activity

Conversation

@claude-for-timatic

@claude-for-timatic claude-for-timatic Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

An activity is now continuous work on one work item, whatever the app or event type. Events on the same customer and ticket within 15 minutes chain into one activity, so coding, testing and reviewing PROJ-12 become one suggestion. Ticketless events keep chaining on customer only. Because an activity now mixes types, activities no longer have an event type.

Closes #41

Breaking API change: ship together with Timatic/timatic-frontend#30 and Timatic/timatic-php-sdk#10.

API contract

  • activities lose the eventTypeId attribute and the eventType relationship.
  • include=activities.eventType on /entry-suggestions is no longer allowed (400).
  • events gain an eventType relationship of type eventTypes, with attribute name.
  • include=activities.events.eventType is allowed and eager-loaded. show loads activities.events.source and activities.events.eventType.
  • The migration drops activities.event_type_id together with its FK and index. Run RebundleSuggestionsCommand after deploy so existing days are re-projected.

Decisions (answers to the open questions)

  • No code_review event type and no chain group. After review, the event type is no longer a boundary: the activity is the work item (decision in the review of this PR).

  • pr_merged counts as work on the ticket and joins the review.

  • Ticketless chaining stays on customer only. Requiring an equal budget would split work where one source knows the budget and another doesn't (unknown is unknown).

  • Neighbour merge. Events are processed by weight, then start time, so two halves of one stretch can be projected separately. After an activity is created or widened, it merges with an activity of the same customer and ticket within 15 minutes. These keep them apart:

    • a booked entry overlapping the merged span
    • another activity in the gap
    • a missing customer, because customerless events are never combined

    Each event is listed once. The activity projected first is kept.

  • Out of scope (Allow overlapping activities when multitasking #33): appendEventToActivity can still widen an activity across a booked entry. This was already the case on main and is pinned in a test.

Review

The first version (a chain_group for review types) was replaced after review on this PR. A code-review agent then reviewed the rework and found:

  • new activities that were never merged
  • duplicate events when the parts of a split event merged
  • a missing entry check on overlapping merges

All three are fixed with tests. Mutation-checked: removing any fix fails its test.

Docs to update after #96 (suggestions/activities.md)

  • Drop "The event type and source on an activity are the label of its strongest event". Types live on the events (activities.events.eventType).
  • "Same ticket within 15 minutes joins…": tests a commit, a pull request comment and an approval on one ticket become one activity in any input order, a merged pull request joins the review on its ticket, adjacent events on different tickets stay separate activities.
  • New bullet: activities of the same customer and ticket that come within 15 minutes merge, because weight order can project two halves separately. A booked entry in the merged span, or another activity in the gap, keeps them apart. Each event is listed once. Tests: the five merge tests in ActivityProjectorTest.
  • Ticketless bullet: drop "whatever that activity's event type".

Verification

Full Pest suite green (627), PHPStan 0 errors, Pint clean.

Comment thread app/Services/ActivityProjector.php Outdated
&& $a->ticket_number === $event->ticket_number
&& $a->event_type_id === $event->event_type_id
&& ($a->event_type_id === $event->event_type_id
|| ($a->eventType?->chain_group !== null && $a->eventType->chain_group === $event->eventType?->chain_group))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could also argue that an activity could consist out of multiple events for the same customer and budget. But that depends on how we exactly define an activity. Is it just some work in a single app? Or would it also be logical if testing in the browser and coding at the same time is considered one activity "Development"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and reworked: an activity is now continuous work on one work item (same customer + ticket within 15 minutes, any app or type). Following your follow-up, activities no longer have an event type at all; the types are read from the activity's events. The PR body has the details and the coordinated frontend/SDK PRs.

A review session spreads over commits, pull request comments, approvals,
change requests and merges. Each event type became its own activity, so
one piece of work on a ticket showed up as several suggestions.

Events on the same customer and ticket within the chain gap now chain
whatever their event type. Because events are projected by weight before
time, an activity that widens can come to lie next to an earlier
projected activity of the same ticket; the two merge unless a booked
entry or another activity lies in the gap between them.

Refs #41
An activity is continuous work on one work item and may now hold events
of several types, so a single event type on the activity no longer
describes it. Clients read the type of each event instead.

BREAKING CHANGE: activities no longer have an `eventTypeId` attribute or
an `eventType` relationship, and `activities.eventType` is no longer an
allowed include on /entry-suggestions. Events now expose an `eventType`
relationship; include it with `activities.events.eventType`.

Refs #41
A newly created activity could end up next to an activity of the same
ticket without either being widened, so the neighbour merge never ran
and one stretch of work stayed split. Every part of a new activity now
looks for neighbours too; customerless activities still merge with
nothing.

Merging lists each event once, as the parts of one split event share
it, and two activities no longer merge into a span that would cover a
booked entry.

Refs #41
@tomasvanrijsse
tomasvanrijsse force-pushed the feat/41-code-review-activity branch from a480d7f to d458bbc Compare November 8, 2026 00:16
@claude-for-timatic claude-for-timatic Bot changed the title feat: group review events into one code review activity feat!: one activity per work item, without an event type Nov 8, 2026
return ! $this->entries->contains(fn (Entry $entry) => $entry->started_at->lt($unionEnd) && $entry->ended_at->gt($unionStart))
&& ($gapEnd->lte($gapStart) || ! $this->activities->contains(fn (Activity $claim) => $claim !== $activity && $claim !== $other
&& $claim->started_at->lt($gapEnd) && $claim->ended_at->gt($gapStart)));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the projector is becoming large, is there a better pattern, a separate class perhaps for each step/rule.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's growing: 288 lines on main, 334 here, 380 with #33. It holds three concerns rather than many rules:

  1. Attribution: customer, budget and internal flag, including learning from earlier bookings.
  2. Overlap: what an activity may claim against entries and other activities.
  3. Chaining and merging.

Plan: a refactor PR stacked on #33 that extracts EventAttribution and ActivityOverlap (plus ActivityChainer if it still feels big). ActivityProjector then only orchestrates the loop.

Not a class per rule: the steps share the activities collection, and their order matters (weight, then the fill pass, then merging). A rule pipeline would hide that order without making it simpler. It comes after #33 because #33 changes exactly these methods.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Group review events as "code review"

1 participant