Files
headroom/openspec/changes/enhanced-allocation/design.md
Santhosh Janardhanan b7bbfb45c0 docs(openspec): add reporting API contract documentation
Add comprehensive API documentation for the reporting endpoint:
- Request/response structure
- View type inference (did/is/will)
- Blank vs explicit zero semantics
- Status values and error responses

Related to enhanced-allocation change.
2026-03-08 18:22:27 -04:00

240 lines
7.8 KiB
Markdown

# Design: Enhanced Allocation Fidelity
## Context
The allocation experience must reflect a strict planning-to-execution model. Current implementation drift introduced incorrect month semantics (`approved_estimate / 12`), mixed concern UIs, and ambiguous status signaling.
This design aligns implementation with the validated operating model:
1. **Projects (Lifecycle)**: `approved_estimate` is the lifecycle total.
2. **Project-Month Plan (Intent)**: manager explicitly sets monthly planned effort per project.
3. **Project-Resource Allocation (Execution)**: for selected month, manager allocates effort by team member.
4. **Reporting (Outcome)**: historical/current/future insights built from these three layers.
## Goals
1. Implement explicit project-month planning as first-class data.
2. Reframe allocation matrix as execution grid with month-plan and capacity variance.
3. Support untracked effort (`team_member_id = null`) end-to-end.
4. Implement partial bulk success for allocations.
5. Keep visual signaling minimal and decision-focused.
## Non-Goals
- Real-time collaboration/websockets.
- Notification systems.
- Export workflows.
- Cosmetic UI experimentation beyond fidelity requirements.
## Decisions
### D1: Month Plan Is Explicit (No Derived Monthly Budget)
- **Decision**: monthly plan is manager-entered and persisted.
- **Rejected**: deriving monthly budget from `approved_estimate / 12`.
- **Reason**: project phasing is dependency-driven, not uniform.
### D2: Reconciliation vs Lifecycle Total
For each project:
- `plan_sum = sum(non-null planned_hours across months)`
- compare against `approved_estimate`
- `plan_sum > approved_estimate` -> `OVER`
- `plan_sum < approved_estimate` -> `UNDER`
- `plan_sum == approved_estimate` -> `MATCH`
Tolerance for MATCH should use decimal-safe comparison in implementation.
### D3: Blank Month Semantics
- Planning grid cell can be blank (`null`) and remains visually blank.
- Allocation variance treats missing month plan as `0`.
- Allocation is allowed when month plan is blank.
### D4: Grid-First Editing
- Project-month planning: inline grid editing primary.
- Allocation matrix: inline grid editing primary.
- Modal is optional/fallback, not primary path.
### D5: Untracked Semantics
- Untracked allocation is represented by `team_member_id = null`.
- Included in project totals and grand totals.
- Excluded from team member capacity/utilization calculations.
### D6: Visual Status Policy
- Over = red
- Under = amber
- Match/settled = neutral
Status emphasis belongs on row/column summary edges and variance cells; avoid noisy color in every interior cell.
### D7: Forecasted Effort Deprecation
- `projects.forecasted_effort` is deprecated for this planning workflow.
- New monthly planning source of truth is explicit project-month plan data.
## Data Model
### New Table: `project_month_plans`
Recommended schema:
- `id` (uuid)
- `project_id` (uuid FK -> projects.id)
- `month` (date, normalized to first day of month)
- `planned_hours` (decimal, nullable)
- `created_at`, `updated_at`
- unique index on (`project_id`, `month`)
Notes:
- `planned_hours = null` means blank/unset plan.
- If storage policy prefers non-null planned hours, clear operation must still preserve blank UI semantics via delete row strategy.
### Existing Tables
- `projects.approved_estimate`: lifecycle cap (unchanged semantics)
- `allocations.team_member_id`: nullable to support untracked
## API Design
### Project-Month Plan APIs
1. `GET /api/project-month-plans?year=YYYY`
- returns month-plan grid payload by project/month
- includes reconciliation status per project
2. `PUT /api/project-month-plans/bulk`
- accepts multi-cell upsert payload
- supports setting value and clearing value (blank)
- returns updated rows + reconciliation results
Example payload:
```json
{
"year": 2026,
"items": [
{ "project_id": "...", "month": "2026-01", "planned_hours": 1200 },
{ "project_id": "...", "month": "2026-02", "planned_hours": 1400 },
{ "project_id": "...", "month": "2026-03", "planned_hours": 400 },
{ "project_id": "...", "month": "2026-04", "planned_hours": null }
]
}
```
### Allocation APIs
- Keep `GET /api/allocations?month=YYYY-MM` and CRUD endpoints.
- Enrich response with month-context variance metadata needed for row/column summary rendering.
- `POST /api/allocations/bulk` must support partial success.
Partial bulk response contract:
```json
{
"data": [{ "index": 0, "id": "...", "status": "created" }],
"failed": [{ "index": 1, "errors": { "allocated_hours": ["..."] } }],
"summary": { "created": 1, "failed": 1 }
}
```
## Computation Rules
### Lifecycle Reconciliation (Project-Month Plan Surface)
For each project:
- `plan_sum = SUM(planned_hours where month in planning horizon and value != null)`
- `status = OVER | UNDER | MATCH` compared to `approved_estimate`
### Allocation Row Variance (Execution Surface)
For selected month and project:
- `allocated_total = SUM(allocation.allocated_hours for project/month including untracked)`
- `planned_month = planned_hours(project, month) or 0 if missing`
- `row_variance = allocated_total - planned_month`
- status from row_variance sign
### Allocation Column Variance (Execution Surface)
For selected month and team member:
- `member_allocated = SUM(allocation.allocated_hours for member/month)`
- `member_capacity = computed month capacity`
- `col_variance = member_allocated - member_capacity`
- exclude `team_member_id = null` rows from this computation
## UX Specification (Implementation-Oriented)
### Surface A: Project-Month Plan Grid
- Rows: projects
- Columns: months
- Right edge: row total + reconciliation status
- Bottom edge: optional month totals for planning visibility
- Cell editing: inline, keyboard-first
- Blank cells remain blank visually
- Color: only summary statuses (red/amber/neutral)
### Surface B: Project-Resource Allocation Grid (Selected Month)
- Rows: projects
- Columns: team members + untracked
- Right edge: project row total + row variance status vs planned month
- Bottom edge: member totals + capacity variance status
- Primary editing: inline cell edit (modal not primary)
- Color: red/amber highlights only for over/under summary states
### Accessibility
- Do not rely on color alone; include text labels (OVER/UNDER).
- Maintain focus and keyboard navigation in both grids.
- Preserve contrast and readable status labels.
## Risk Register
1. **Semantic regression risk**: reintroduction of derived monthly budget logic.
- Mitigation: explicit tests and prohibition in specs.
2. **Blank vs zero confusion**.
- Mitigation: explicit API contract and UI behavior tests.
3. **Untracked leakage into capacity metrics**.
- Mitigation: query filters and dedicated tests.
4. **Bulk partial side effects**.
- Mitigation: per-item validation and clear response contract.
## Testing Strategy
### Unit
- Lifecycle reconciliation calculator
- Row/column variance calculators
- Blank-plan-as-zero execution logic
### Feature/API
- Project-month plan bulk upsert and clear behavior
- Reconciliation status correctness
- Allocation month variance data correctness
- Untracked include/exclude behavior
- Partial bulk success semantics
### Frontend Grid Tests
- Inline edit commit/cancel/clear
- Keyboard navigation
- Summary status placement and labels
- Blank visual state preservation
### E2E
- Create lifecycle estimate project
- Enter monthly plan across months and verify reconciliation
- Execute month allocations and verify row/column variances
- Validate untracked behavior
- Validate partial bulk success handling
## Migration & Rollout Notes
1. Introduce project-month plan model and APIs.
2. Remove derived budget rendering in allocation UI.
3. Wire allocation UI to explicit month plan for variance.
4. Deprecate `forecasted_effort` usage in this workflow path.
5. Keep backward compatibility for existing allocation records.