Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
144
planned
1,362
shipped

Motir · Work items

MOTIR-225Done

4.3 Story-point estimation

The estimation layer of Epic 4 — Jira-faithful story-point estimation that fills the points seams the rest of the epic reserved. Four things ship: (1) a numeric work_item.storyPoints field (SEPARATE from the existing estimateMinutes TIME estimate); (2) a project-scoped estimation config — which statistic is THE estimate (Story Points default · Time · Issue count) plus the configurable point scale/deck (Fibonacci · linear · custom); (3) an inline-editable estimate badge wherever issues render (backlog row, board/scrum card, issue detail rail, list); and (4) bounded roll-ups to the sprint (committed / completed / remaining points) and epic (sum over the subtree) level. It owns the estimate ENTITY + the roll-up engine; the surfaces it decorates were built by earlier stories.

What 4.3 fills (the seam wiring — all backward deps). Epic 4 deliberately deferred every points concern to this story (the mistake-#32 resolution that kept 4.2 / 4.5 forward-audit-clean). 4.3 redeems them: it fills the backlog row estimate slot + the per-sprint committed-points slot that design/backlog/design-notes.md names "filled by Story 4.3" (Story 4.2 drew them as reserved dashed seams); it generalises the board .pts card chip (currently rendering estimateMinutes) to the configured statistic; and it provides the storyPoints field + the reusable rollupForSprint aggregate that Story 4.5.2 reads for its SprintSummaryDto.points. 4.3 > 4.2 and 4.5 > 4.3, so every one of these is a normal backward dep — no forward-pointing dependency, no re-plan of 4.2 / 4.5.

estimateMinutes vs storyPoints (two fields, one statistic switch). estimateMinutes (time, Story 2.3.6) already exists and already has its own editing (issue detail). storyPoints is the NEW agile estimate. The project estimation statistic picks which of {Story Points · Time · Issue count} the planning surfaces DISPLAY and the roll-ups SUM — exactly Jira board Estimation settings (decision-ladder rung 1). Default Story Points. The roll-up engine takes the statistic as a parameter (SUM(storyPoints) | SUM(estimateMinutes) | COUNT(*)), so the switch is one parameter, not three code paths.

Estimation config is PROJECT-scoped (justified rung-1 deviation — see the module header). Jira configures estimation per-board; this product makes boards per-project read projections and sprints project-scoped (Story 4.1), so the planning estimate is a project property: project.estimationStatistic + pointScale + customScaleValues, edited at settings/project/estimation (sibling of the workflow + board settings panels). Modelling it per-board would force a board to exist before estimating and split estimation across boards — added complexity, no real use case here.

The point SCALE. Story points stay a free numeric value (decimals allowed, Jira-faithful) so they always roll up. The configurable scale is the suggested deck the picker offers (it does not hard-constrain entry): Fibonacci (default — 1,2,3,5,8,13,21,…), linear (1,2,3,4,5,…), or custom (customScaleValues). The estimate picker shows the deck as quick-pick chips + a free numeric input + a clear action. T-shirt sizes are out of scope (non-numeric, would not roll up).

Roll-ups are BOUNDED aggregates (finding #57). The sprint roll-up is a grouped SUM(storyPoints) over the sprint's issues — committed = total, completed = sum scoped to issues whose status maps to a category = 'done' workflow status (the finding-#21 terminal-set predicate), remaining = committed − completed. The epic roll-up is a recursive-CTE SUM over the parent's subtree (points roll up through intermediate stories into the epic, Jira-style). Both are single bounded queries — NEVER a load-all + client sum (the prototype tell finding #57 forbids). Per-issue estimate writes are single-row. NULL-estimate issues contribute 0; a wholly unestimated sprint/epic returns 0 (the DTO stays total; the UI owns the "—" presentation, matching 4.5's documented degradation).

4-layer + tenancy (CLAUDE.md). The storyPoints write + the config read/update are repository single-ops (writes require tx); the roll-up aggregates are repository $queryRaw reads; the service owns the transactions + DTO mapping + typed errors + the finding-#26 workspaceId gate on every read/write. Each estimate change records a work_item_revision row (reuse the Story 1.4.6 audit service) in the SAME transaction, so the activity feed (Story 5.5) and reporting (Epic 6) see estimate changes for free. storyPoints stays camelCase (no @map) to match work_item's existing camelCase columns (decision-ladder rung 2 — within-table consistency, the same call the schema's 4.1 sprintId/backlogRank comment records); no new FK, so the FK-drift rule is n/a but a re-run of migrate dev must still report "No difference detected".

Design gate. The estimate PICKER, the story-points detail field, the epic roll-up badge, and the project Estimation settings panel are unspecified (== no design), so subtask 4.3.1 creates a NEW design/estimation/ area FIRST and every UI code subtask depends on it (seeded blocked).

Out of scope (Epic-4 siblings / Epic 6 / later): the sprint START / COMPLETE flows + sprint report (Story 4.4); the Scrum BOARD view + the SprintSummaryDto.columnPoints per-column breakdown (Story 4.5 — it READS 4.3's field + reuses rollupForSprint, but the scrum render is 4.5); the velocity + burndown charts (Story 4.6 — 4.3 ships numeric roll-ups, leaves the chart seam); the backlog rank / association / bounded reads themselves (Story 4.1); the backlog grooming UI (Story 4.2 — consumed, its seams filled); time-estimate editing (estimateMinutes, Story 2.3.6 — already shipped); a separate planning-poker / voting session (not a Jira-core feature — out of scope); t-shirt (non-numeric) scales; estimation REPORTS beyond the inline roll-ups (Epic 6 dashboards).

Verification

  • Pull the Story branch, pnpm install, pnpm prisma migrate dev (applies the add_story_points_and_estimation_config migration), pnpm db:seed, pnpm dev. (Requires Story 4.2 merged so the backlog surface whose seams 4.3 fills exists.)
  • Migration is clean (no drift): a second pnpm prisma migrate dev reports "No difference detected" (work_item.storyPoints + the project estimation columns + the two enums add no spurious change). pnpm prisma migrate status is up to date.
  • Design exists first: design/estimation/estimation.mock.html + estimation-settings.mock.html + PNG export(s) + design/estimation/design-notes.md exist (subtask 4.3.1), built from components/ui/* + --el-*/element-shape tokens only, AA-safe, passing the render checklist; they draw the editable estimate badge (display + click-to-edit picker with the scale deck + free numeric + clear), the epic roll-up badge, the sprint committed-points roll-up filling the backlog header slot, and the project Estimation settings panel.
  • pnpm test:coverage — Vitest (real Postgres) over the estimate write + the estimation config + the bounded sprint/epic roll-ups stays ≥90% per-file branch/fn/line on the new service/repository files (the CI coverage gate, prodect-core-coverage-gate); empty-input guards on any new repo method have a direct test.
  • Estimate write: setting a story-point estimate on an issue writes a single work_item.storyPoints row, records a 1.4.6 revision in the same transaction, and is denied by the finding-#26 workspaceId gate cross-workspace; clearing it nulls the field.
  • Config: the project Estimation settings panel (settings/project/estimation) reads/writes estimationStatistic (default Story Points) + pointScale (default Fibonacci) + customScaleValues; switching the statistic to Time / Issue count changes which value the planning surfaces show + the roll-ups sum; admin-only.
  • Inline badge: the estimate badge renders the configured statistic on the backlog row (filling the Story-4.2 estimate seam with no relayout), the board/scrum card (.pts chip, now the configured statistic, not raw estimateMinutes), the issue detail rail (a story-points field distinct from the time Estimate), and the list; click-to-edit shows the scale deck as quick chips + a free numeric input + clear; the write is optimistic with snap-back on error.
  • Sprint roll-up (fills the 4.2 committed-points seam): a sprint container header shows committed / (and the seam for) completed / remaining points from the BOUNDED rollupForSprint aggregate; completed counts only issues in a category = 'done' status; a wholly unestimated sprint shows "—" (no NaN); the figure is NOT a sum over the loaded row page.
  • Epic roll-up: an epic (or any parent) shows a rolled-up point total = the recursive SUM over its subtree (a story's points roll into its epic through intermediate levels), on the issue detail + list/tree; the read is one bounded recursive-CTE aggregate.
  • Scale check (finding #57): pnpm db:seed:large (a project with a large sprint + deep epic subtree) → the sprint + epic roll-ups return from one bounded aggregate each (no load-all), and the inline estimate writes stay O(1); the backlog/board DOM row count stays bounded.
  • pnpm test:e2e --grep estimation — Playwright: estimate a backlog story via the inline picker (the badge updates), see the sprint container committed-points figure increase, open the issue detail and see the story-points field, and see the parent epic's rolled-up total reflect the change.
  • 4.5 seam check: rollupForSprint(sprintId, statistic) is exported as a reusable bounded aggregate that Story 4.5.2's SprintSummaryDto.points consumes (documented in design/estimation/design-notes.md + 4.5.2's notes) — 4.3 does not duplicate the scrum columnPoints breakdown.
  • a11y / tokens: the estimate badge + roll-up figures read as TEXT (number + label, not colour/shape alone — finding #35); colour via --el-*, shape via element shape tokens (no Tier-0 --color-* / raw rounded-*motir-core/CLAUDE.md); the picker is keyboard-operable.