Task — engineering-spec@1

"RES hiring memo CUO draft — skill-gap × CRM pipeline trigger → CEO+CFO review queue with cost-benefit projection"

draftTASK-RES-004
module res · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-CUO-101, TASK-CRM-001 · blocks none

§1 — Description (BCP-14 normative)

The RES service MUST ship hiring memo at services/res/src/hiring/ triggered by skill-gap + pipeline + cost projection + CEO+CFO dual-sign, 4 memory audit kinds.

  1. MUST validate hire_recommendation against closed enum per DEC-2071.
  1. MUST detect trigger at gap_detector.rs::detect(tenant) per DEC-2070:
  1. MUST project cost at cost_projector.rs::project(role, region, contract_type) per DEC-2072:
  1. MUST generate memo at memo_generator.rs::generate(tenant, gap, costs):
  1. MUST require CEO + CFO dual-sign per DEC-2073 — same-person rejected.
  1. MUST define table at migration 0004: ``sql CREATE TABLE res_hiring_memos ( memo_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, trigger_kind TEXT NOT NULL, -- 'auto_gap_pipeline' | 'manual_ceo' | 'manual_chro' role_title TEXT NOT NULL, region TEXT NOT NULL, proposed_contract_type TEXT, memo_body_jsonb JSONB NOT NULL, recommendation TEXT NOT NULL CHECK (recommendation IN ('hire_immediate','hire_q_plus_1','defer_pipeline_uncertain','reject_no_business_case')), status TEXT NOT NULL DEFAULT 'pending_review' CHECK (status IN ('pending_review','ceo_signed','cfo_signed','approved_for_action','dismissed')), ceo_signed_by UUID, ceo_signed_at TIMESTAMPTZ, cfo_signed_by UUID, cfo_signed_at TIMESTAMPTZ, hire_slot_reserved BOOLEAN NOT NULL DEFAULT false, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE res_hiring_memos ENABLE ROW LEVEL SECURITY; CREATE POLICY hiring_memos_rls ON res_hiring_memos USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON res_hiring_memos FROM cyberos_app; GRANT UPDATE (status, ceo_signed_by, ceo_signed_at, cfo_signed_by, cfo_signed_at, hire_slot_reserved) ON res_hiring_memos TO cyberos_app; ``
  1. MUST expose endpoints: ``text POST /v1/res/hiring-memos (CEO/CHRO manual trigger) POST /v1/res/hiring-memos/{id}/ceo-sign POST /v1/res/hiring-memos/{id}/cfo-sign POST /v1/res/hiring-memos/{id}/dismiss GET /v1/res/hiring-memos (list) ``
  1. MUST emit 4 memory audit kinds per DEC-2074. PII per TASK-MEMORY-111: cost projections SHA-256 hashed.
  1. MUST thread trace_id from trigger → projector → memo → sign → audit.
  1. MUST NOT auto-action without dual-sign per DEC-2073.
  1. MUST NOT allow same person to sign both roles per DEC-2073.

§2 — Why this design

Why dual-sign (DEC-2073)? Headcount = major financial commitment; CFO budget perspective + CEO strategic both required.

Why cost projection (DEC-2072)? Without numbers, debate is opinion; projection grounds discussion.

Why pipeline correlation (DEC-2070)? Over-allocation alone may be temporary; pairing with deal velocity confirms sustained demand.

Why 4 recommendations (DEC-2071)? Captures real decisions — immediate, next quarter, defer, reject — bounded prevents waffling.


§3 — API contract

Sample memo:

{
  "memo_id": "uuid",
  "role_title": "Senior Backend Engineer",
  "region": "vn-1",
  "memo_body_jsonb": {
    "skill_gap": "Backend capacity at 115% for 6 weeks",
    "pipeline_correlation": "$300k in committed deals requiring backend work in next quarter",
    "cost_projection": {
      "annual_base_vnd": 600000000,
      "annual_si_employer_vnd": 144000000,
      "annual_total_vnd": 744000000,
      "6mo_total_vnd": 372000000
    },
    "roi_estimate": "Break-even at month 2 if pipeline holds",
    "ai_recommendation": "hire_immediate"
  },
  "recommendation": "hire_immediate",
  "status": "pending_review"
}

§4 — Acceptance criteria

  1. hire_recommendation enum cardinality 4. 2. Auto-trigger at 4w over-allocation + pipeline velocity. 3. Manual trigger CEO/CHRO. 4. Cost projection includes statutory. 5. CEO + CFO dual-sign required. 6. Same-person rejected. 7. approved_for_action requires both signs. 8. hire_slot_reserved set on approval (capacity matrix marker). 9. 4 memory audit kinds emitted. 10. PII scrubbed (cost SHA256). 11. RLS denies cross-tenant. 12. Trace_id preserved. 13. Append-only via REVOKE except status cols. 14. AI failure → minimal memo + sev-2. 15. Dismiss allowed at any pre-action stage. 16. Status workflow enforced. 17. Auto-trigger idempotent per tenant per week. 18. Memo body JSONB schema validated. 19. Region required (drives cost projection). 20. List endpoint paginated.

§5 — Verification

#[tokio::test]
async fn auto_trigger_at_4w_over_allocation_plus_pipeline() {
    let ctx = TestContext::with_4w_over_allocation_and_new_deals().await;
    ctx.run_hiring_detector(ctx.tenant_id).await;
    let memos = ctx.fetch_hiring_memos(ctx.tenant_id).await;
    assert!(!memos.is_empty());
}

#[tokio::test]
async fn dual_sign_required() {
    let ctx = TestContext::with_pending_memo().await;
    ctx.ceo_sign(ctx.memo_id).await;
    let memo = ctx.fetch_memo(ctx.memo_id).await;
    assert_eq!(memo.status, "ceo_signed");
    ctx.cfo_sign(ctx.memo_id).await;
    let memo2 = ctx.fetch_memo(ctx.memo_id).await;
    assert_eq!(memo2.status, "approved_for_action");
    assert!(memo2.hire_slot_reserved);
}

#[tokio::test]
async fn same_person_both_roles_rejected() {
    let ctx = TestContext::with_pending_memo().await;
    ctx.ceo_sign_as(ctx.user_a, ctx.memo_id).await;
    let r = ctx.try_cfo_sign_as(ctx.user_a, ctx.memo_id).await;
    assert!(r.is_err());
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-CUO-101, TASK-CRM-001. Cross-module: TASK-RES-001 (over-allocation signal), TASK-RES-002 (slot reserved), TASK-HR-002 (contract type), TASK-REW-004 (statutory deductions), TASK-AI-003 (LLM), TASK-AUTH-101 (CEO/CFO roles), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
AI timeoutretry 1xminimal memo + sev-2CEO completes manually
Cost projection failcatchsev-2; use rough estimatedata fix
One signer missinggateinherentwait for second
Same-person dual-signvalidate403different signer
Auto-trigger noiserate limit per tenant/weekinherentinherent
Pipeline data stalewarnproceed with sev-3data refresh
Cross-tenant memoRLS403inherent
hire_slot reservation raceUPDATE WHEREfirst winsinherent
Stage revert post-approvalmanual CEO actioninherentreject memo
Region invalidvalidate400use valid

§11 — Implementation notes


End of TASK-RES-004 spec.