Skip to content

feat(time): answer the week over the time contract - #154

Merged
Bakhomious merged 1 commit into
mainfrom
step/time-week
Oct 29, 2026
Merged

Bakhomious merged 1 commit into
mainfrom
step/time-week

Conversation

@Bakhomious

Copy link
Copy Markdown
Contributor

Brief

Goal. The time service answers TimeService.GetWeek, from the second contract between services, bakhyard.time.v1 (plan §4.5). It returns this week's hours against the week's target, for the Home's "This week". home asks for it in the next step.

Scope.

  • proto/bakhyard/time/v1/time.proto: TimeService.GetWeek (NO_SIDE_EFFECTS) returns GetWeekResponse{week}, and Week{logged_hours, target_hours} is in hours. The generated Go is in internal/gen/bakhyard/time/v1.
  • services/time/internal/week: Server answers the week from the latest poll to succeed, once the first poll has ended:
    • no week without Jira;
    • Unavailable before any poll has succeeded;
    • the context's error if the context ends first.
  • services/time/main.go mounts it on time's router, next to GET /time/report, on the same poller.
  • Tests: unit tests with testing/synctest, and one test through routes over gRPC on HTTP/2 without TLS (h2c).
  • Docs: plan §0, §4.5, §8.2 and decision 185. The next step's id, home-week, is already a Phase 1 step's (feat(web): show the week's hours on the Home #24), so §8.2 and decision 183 now call it home-time-week.

Out of scope.

  • home asking for the week, and the Home showing it or "Hours unavailable" (the next step).
  • time's side of the attention contract (time-attention).
  • TypeScript from the contracts, and buf breaking (decision 158).

Acceptance criteria.

  1. buf lint and buf format pass, and mise run gen:check passes with the generated code committed.
  2. time answers TimeService/GetWeek over gRPC on h2c on its router, alongside GET /time/report and /healthz.
  3. With Jira set up, the week is the report's week from the latest poll to succeed: the same hours as GET /time/report's week, kept through later failed polls.
  4. Without Jira, GetWeek answers with no week.
  5. With Jira set up, before any poll has succeeded, GetWeek fails with Unavailable.
  6. A call waits for the first poll to end, and returns its context's error if the context ends first.
  7. mise run check passes and the PR's CI is green.

QA

  • Mutations: 18 of 18 caught (files/qa-time-week/mutate.py):
    • week.go: no nil check; a zero week, or UNAVAILABLE, without Jira; the context's error swallowed, or turned into UNAVAILABLE; the success check removed, inverted, or replaced by "unavailable after any failure"; another code; another message; logged and target hours swapped, either one missing, or the logged hours as the target;
    • main.go: not mounted; mounted on another mux; the week server without the poller; a poller of its own.
    • The last one survived at first: a second poller answers the same week in the tests, while it would read Jira twice as often, and its answers could differ from the report's. QA made the stand-in Jira log an hour more with each read, so TestTimeAnswersTheWeekOfItsReport now fails when the week and the report come from different reads.
  • Live (live.py, 25 checks, all passed): the service's binary against a stand-in Jira on the loopback, called over gRPC on h2c with buf curl and the repo's schema.
    • AC4: without Jira, GetWeek answers no week ({}), and GET /time/report still answers 404. Nothing is logged.
    • AC2, AC3: with Jira, 1.5 of 40 hours, the same as GET /time/report's week. The Connect protocol over HTTP/1.1 answers the same, as pulls' and todos' handlers do; the gateway doesn't route either. With nothing logged, the week says 0 hours rather than no week.
    • AC5: with Jira refusing from the start, unavailable with the message, and poll Jira worklogs logged once.
    • AC6: with a first read that takes 3 s, the call waits for it and then answers the week. With a 1 s deadline, sent as grpc-timeout, the call ends with deadline_exceeded.
    • AC3: a QA build that polls every 3 s (go build -overlay) keeps the week of the last read to succeed after a failed read, and answers the new week once a read succeeds again.
    • Each run stops on SIGTERM with exit 0.
  • -race -count=40 on the new tests: no failure.
  • A service that stops during its first read releases the calls waiting for it with UNAVAILABLE, since no read has succeeded; that fits the code's meaning.

Review

No blocking findings. Non-blocking:

  • A kept week stays until a read succeeds, even past Monday. The Time page says how old its hours are; the Home won't (decision 185).
  • The contract's comment says home calls it, as the attention contract's did before home-fanout. It becomes true with the next step.
  • The stand-in Jira in the service's tests now logs an hour more with each read, for the second-poller check.

Gates

mise run check passed locally:

  • Go fmt; golangci-lint (0 issues); buf lint and format; jactionlint; process-compose dry run; gen:check; govulncheck (none);
  • Go tests with -race, Go coverage 95.9% total (services/time/internal/week 100%);
  • Biome, svelte-check, tsc, knip, pnpm audit;
  • Vitest, 1,102 tests;
  • Playwright E2E, 337 passed and 7 skipped.

SonarQube Cloud runs on demand (decision 132).

Merged under the user's standing OK (2026-10-26): merge once CI's check is green, and review everything at the end.

Home's This week card needs the week's hours from time. A new internal
gRPC contract, bakhyard.time.v1, answers the week of the report time
already polls from Jira: no week without Jira, UNAVAILABLE until a read
of the worklogs succeeds, and the last good week through failed reads
(decision 185).
@Bakhomious
Bakhomious merged commit 3c9a23c into main Oct 29, 2026
2 checks passed
@Bakhomious
Bakhomious deleted the step/time-week branch October 29, 2026 16:08
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.

1 participant