Task — engineering-spec@1

"RES capacity-vs-demand matrix — nightly join across HR + PROJ + TIME + LEARN producing per-member-week capacity/demand grid"

draftTASK-RES-001
module res · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-HR-001, TASK-PROJ-001, TASK-TIME-001 · blocks TASK-RES-002, TASK-RES-003

§1 — Description (BCP-14 normative)

The RES service MUST ship capacity matrix at services/res/src/matrix/ joining HR + PROJ + TIME + LEARN nightly, per-member-week grid, 4 memory audit kinds.

  1. MUST validate matrix_run_status against closed enum per DEC-2032.
  1. MUST compute at computer.rs::compute(member, iso_week) per DEC-2031:
  1. MUST run batch at batch_runner.rs::run(tenant, run_date) per DEC-2030:
  1. MUST define table at migration 0001: ```sql CREATE TABLE res_capacity_matrix ( matrix_row_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, member_id UUID NOT NULL, iso_week CHAR(8) NOT NULL, project_id UUID, -- NULL for member-total row capacity_hours NUMERIC(5,2) NOT NULL, demand_hours NUMERIC(5,2) NOT NULL, allocated_hours NUMERIC(5,2) NOT NULL, run_date DATE NOT NULL, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, member_id, iso_week, project_id, run_date) ); CREATE INDEX matrix_member_week_idx ON res_capacity_matrix(tenant_id, member_id, iso_week); ALTER TABLE res_capacity_matrix ENABLE ROW LEVEL SECURITY; CREATE POLICY matrix_rls ON res_capacity_matrix USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON res_capacity_matrix FROM cyberos_app;

CREATE TABLE res_matrix_runs ( run_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, run_date DATE NOT NULL, status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running','completed','partial','failed')), members_total INT NOT NULL DEFAULT 0, members_succeeded INT NOT NULL DEFAULT 0, started_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ, trace_id CHAR(32), UNIQUE (tenant_id, run_date) ); ALTER TABLE res_matrix_runs ENABLE ROW LEVEL SECURITY; CREATE POLICY matrix_runs_rls ON res_matrix_runs USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON res_matrix_runs FROM cyberos_app; GRANT UPDATE (status, members_total, members_succeeded, completed_at) ON res_matrix_runs TO cyberos_app; ```

  1. MUST schedule cron 04:00 tenant_tz via TASK-MCP-007.
  1. MUST be idempotent per DEC-2033 (UNIQUE constraint).
  1. MUST expose endpoints: ``text POST /v1/res/matrix/trigger (CHRO manual trigger) GET /v1/res/matrix/runs/{id} (status) GET /v1/res/members/{id}/capacity (member capacity view) ``
  1. MUST emit 4 memory audit kinds per DEC-2034. PII per TASK-MEMORY-111: hours SHA-256 hashed.
  1. MUST thread trace_id from cron → batch → per-member compute → audit.
  1. MUST NOT mutate prior matrix row per DEC-2030 (append-only).
  1. MUST NOT double-count training hours (LEARN deducts from capacity, not demand).

§2 — Why this design

Why join 4 modules (DEC-2030)? True capacity needs all sources — HR provides headcount, PROJ demand, TIME actual, LEARN time-off-for-training.

Why per-week (DEC-2030)? Weekly granularity matches sprint cadence; daily too noisy, monthly too coarse.

Why nightly (DEC-2030)? Allocation decisions need fresh data Monday; nightly batch guarantees ≤24h lag.

Why idempotent (DEC-2033)? Cron retry must not double-count; UNIQUE enforces.


§3 — API contract

Sample capacity view:

{
  "member_id": "uuid",
  "weeks": [
    {
      "iso_week": "2026-W20",
      "capacity_hours": 40,
      "demand_hours": 35,
      "allocated_hours": 33,
      "utilization_pct": 82.5,
      "per_project": [
        {"project_id": "uuid-a", "allocated_hours": 20},
        {"project_id": "uuid-b", "allocated_hours": 13}
      ]
    }
  ]
}

§4 — Acceptance criteria

  1. matrix_run_status enum cardinality 4. 2. Capacity = hours_per_week - PTO - LEARN. 3. Demand = sum PROJ-013 estimates. 4. Allocated = sum TIME entries. 5. Per-member 12-week forecast. 6. Per-project breakdown row + total row. 7. Nightly cron 04:00. 8. Idempotent via UNIQUE. 9. Per-member failure isolated. 10. 4 memory audit kinds emitted. 11. PII scrubbed (hours SHA256). 12. RLS denies cross-tenant. 13. CHRO-only manual trigger. 14. Trace_id preserved. 15. Append-only matrix table. 16. rust_decimal precision. 17. Inactive member skipped. 18. Contract type override respected (TASK-HR-002). 19. Run status transitions correct. 20. Empty tenant skipped.

§5 — Verification

#[tokio::test]
async fn capacity_minus_pto_and_learn() {
    let ctx = TestContext::member_40h_with_pto_4h_learn_2h().await;
    ctx.run_matrix_batch(today()).await;
    let row = ctx.fetch_matrix_row(ctx.member_id, this_week()).await;
    assert_eq!(row.capacity_hours, dec!(34.0));  // 40 - 4 - 2
}

#[tokio::test]
async fn idempotent_double_run() {
    let ctx = TestContext::with_active_member().await;
    ctx.run_matrix_batch(today()).await;
    ctx.run_matrix_batch(today()).await;
    let rows = ctx.fetch_matrix_rows(ctx.member_id, this_week()).await;
    let count = rows.iter().filter(|r| r.run_date == today()).count();
    assert!(count <= 12);  // 12 weeks per member; not doubled
}

#[tokio::test]
async fn per_member_failure_isolated() {
    let ctx = TestContext::with_5_members_one_will_fail().await;
    ctx.run_matrix_batch(today()).await;
    let run = ctx.fetch_latest_run().await;
    assert_eq!(run.members_succeeded, 4);
    assert_eq!(run.status, "partial");
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-HR-001, TASK-PROJ-001, TASK-TIME-001. Downstream: TASK-RES-002 (Gantt UI), TASK-RES-003 (over/under flags), TASK-RES-005 (OT cap check). Cross-module: TASK-HR-002 (contract type), TASK-HR-004 (PTO), TASK-LEARN-001 (training), TASK-PROJ-013 (estimates), TASK-MCP-007 (cron), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Source module unavailablecatchsev-2; null demandretry
Cron skippedcatch-up nextinherentinherent
Duplicate runUNIQUEskipinherent
Decimal precision driftrust_decimalinherentinherent
Member just hiredno historyuse current contractinherent
0 active membersskipinherentinherent
Cross-tenant queryRLS0 rowsinherent
Mid-batch crashresume from lastpartialretry
Future week with no projectsinherentdemand=0inherent
Negative capacity (over-PTO)clamp at 0sev-3data fix

§11 — Implementation notes


End of TASK-RES-001 spec.