Task — engineering-spec@1

"RES over/under-allocation flags — 110% warning / 60% under-utilization threshold with weekly digest to CHRO"

draftTASK-RES-003
module res · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-RES-001 · blocks none

§1 — Description (BCP-14 normative)

The RES service MUST ship flag computation + weekly digest at services/res/src/flags/ with 110/60 thresholds + Friday CHRO digest, 2 memory audit kinds.

  1. MUST validate allocation_flag against closed enum per DEC-2051.
  1. MUST compute flag at computer.rs::compute(matrix_row) per DEC-2050:
  1. MUST cache flag on TASK-RES-001 matrix row: ``sql ALTER TABLE res_capacity_matrix ADD COLUMN allocation_flag TEXT CHECK (allocation_flag IS NULL OR allocation_flag IN ('over_allocated','healthy','under_utilized')); ALTER TABLE res_capacity_matrix ADD COLUMN flag_computed_at TIMESTAMPTZ; GRANT UPDATE (allocation_flag, flag_computed_at) ON res_capacity_matrix TO cyberos_app; ``
  1. MUST schedule weekly digest Friday 16:00 tenant_tz per DEC-2053 via TASK-MCP-007 — count flagged members + send to CHRO via TASK-EMAIL-009 + TASK-CHAT-005.
  1. MUST define digest table at migration 0003: ``sql CREATE TABLE res_weekly_digests ( digest_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, iso_week CHAR(8) NOT NULL, over_count INT NOT NULL, under_count INT NOT NULL, healthy_count INT NOT NULL, flagged_members_jsonb JSONB NOT NULL, sent_at TIMESTAMPTZ, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, iso_week) ); ALTER TABLE res_weekly_digests ENABLE ROW LEVEL SECURITY; CREATE POLICY digests_rls ON res_weekly_digests USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON res_weekly_digests FROM cyberos_app; GRANT UPDATE (sent_at) ON res_weekly_digests TO cyberos_app; ``
  1. MUST expose endpoints: ``text GET /v1/res/flags/summary (current week counts) GET /v1/res/weekly-digests (list) ``
  1. MUST emit 2 memory audit kinds per DEC-2054. PII per TASK-MEMORY-111: flag enums (public) ok; counts ok.
  1. MUST thread trace_id from compute / cron → digest → audit.
  1. MUST NOT hardcode thresholds outside DEC-2050 (consider future per-tenant override).

§2 — Why this design

Why 110%/60% (DEC-2050)? Industry standard; balance burnout prevention + bench cost.

Why cache on matrix row (DEC-2052)? Avoids recomputing on every query; UI shows flag with row.

Why weekly digest (DEC-2053)? Daily would be noise; monthly too late.


§3 — API contract

Sample summary:

{
  "iso_week": "2026-W20",
  "over_count": 3,
  "healthy_count": 12,
  "under_count": 2,
  "over_members": [
    {"member_id": "uuid", "name": "Alice", "utilization_pct": 115}
  ]
}

§4 — Acceptance criteria

  1. allocation_flag enum cardinality 3. 2. >=110% → over_allocated. 3. <60% → under_utilized. 4. Threshold boundaries exact (110 and 60 inclusive of healthy edge). 5. Flag cached on matrix row. 6. Weekly digest Friday 16:00. 7. Digest sent via email + chat. 8. UNIQUE(tenant_id, iso_week) idempotency. 9. 2 memory audit kinds emitted. 10. PII: flag enums + counts ok. 11. RLS denies cross-tenant. 12. Trace_id preserved. 13. Append-only via REVOKE except sent_at. 14. 0-capacity member → flag=null + sev-3. 15. Recompute on TASK-RES-002 commit. 16. Recompute on TASK-RES-001 batch. 17. Digest skip if 0 members. 18. rust_decimal precision for utilization. 19. CHRO-only digest config. 20. flag_computed_at populated.

§5 — Verification

#[tokio::test]
async fn over_allocated_at_115pct() {
    let row = matrix_row_with(capacity: 40, allocated: 46);
    assert_eq!(flags::compute(&row), Flag::OverAllocated);
}

#[tokio::test]
async fn under_at_50pct() {
    let row = matrix_row_with(capacity: 40, allocated: 20);
    assert_eq!(flags::compute(&row), Flag::UnderUtilized);
}

#[tokio::test]
async fn weekly_digest_sent_friday() {
    let ctx = TestContext::with_3_over_2_under_members().await;
    ctx.run_friday_digest_cron().await;
    let digest = ctx.fetch_latest_digest().await;
    assert_eq!(digest.over_count, 3);
    assert_eq!(digest.under_count, 2);
    let email_sent = ctx.email_send_count().await;
    assert_eq!(email_sent, 1);
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-RES-001. Cross-module: TASK-RES-002 (UI hook to recompute), TASK-EMAIL-009, TASK-CHAT-005, TASK-MCP-007, TASK-AUTH-101, TASK-MEMORY-111.

§10 — Failure modes

FailureDetectionOutcomeRecovery
0 capacitynull flag + sev-3inherentdata fix
Cron skippedcatch-upinherentinherent
Duplicate digestUNIQUEskipinherent
Email send failsev-2retryinherent
Chat send failsev-2retryinherent
Negative allocationflag=under (clamp 0)sev-3data fix
Decimal precisionrust_decimalinherentinherent
Cross-tenant queryRLS0 rowsinherent
Threshold mid-batch changeinherentinherentfuture tenant override
Bench member (contractor)inherent flag computedinherentinherent

§11 — Implementation notes


End of TASK-RES-003 spec.