Task — engineering-spec@1

"ESOP Good/Bad Leaver branch on HR offboarding — CFO+CEO co-sign to apply forfeiture/acceleration per termination_kind"

draftTASK-ESOP-005
module esop · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-HR-009 · blocks none

§1 — Description (BCP-14 normative)

The ESOP service MUST ship GL/BL branch at services/esop/src/leaver/ triggered by TASK-HR-009 with CFO+CEO dual-sign + forfeiture executor, 5 memory audit kinds.

  1. MUST validate leaver_outcome against closed enum per DEC-2291.
  1. MUST trigger on TASK-HR-009 termination.executed per DEC-2290.
  1. MUST require CFO+CEO dual-sign at dual_sign_gate.rs per DEC-2292 — same-person rejected.
  1. MUST execute forfeiture at forfeiture_executor.rs::execute(outcome) per DEC-2290:
  1. MUST define table at migration 0005: ``sql CREATE TABLE esop_leaver_outcomes ( outcome_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, member_id UUID NOT NULL, termination_id UUID NOT NULL, -- TASK-HR-009 ref grant_id UUID NOT NULL REFERENCES esop_sp_grants(grant_id), outcome TEXT NOT NULL CHECK (outcome IN ('good_leaver_full_vest','good_leaver_pro_rated','bad_leaver_unvested_forfeit','bad_leaver_full_forfeit','mutual_negotiated')), shares_vested_at_term BIGINT NOT NULL, shares_forfeited BIGINT NOT NULL, shares_retained BIGINT NOT NULL, status TEXT NOT NULL DEFAULT 'drafted' CHECK (status IN ('drafted','cfo_signed','ceo_signed','committed','dismissed')), cfo_signed_by UUID, cfo_signed_at TIMESTAMPTZ, ceo_signed_by UUID, ceo_signed_at TIMESTAMPTZ, committed_at TIMESTAMPTZ, notes TEXT, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (termination_id, grant_id) ); ALTER TABLE esop_leaver_outcomes ENABLE ROW LEVEL SECURITY; CREATE POLICY outcomes_rls ON esop_leaver_outcomes USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON esop_leaver_outcomes FROM cyberos_app; GRANT UPDATE (status, cfo_signed_by, cfo_signed_at, ceo_signed_by, ceo_signed_at, committed_at) ON esop_leaver_outcomes TO cyberos_app; ``
  1. MUST expose endpoints: ``text POST /v1/esop/leaver-outcomes (auto from TASK-HR-009 trigger; CFO/CEO can also draft) POST /v1/esop/leaver-outcomes/{id}/cfo-sign POST /v1/esop/leaver-outcomes/{id}/ceo-sign GET /v1/esop/leaver-outcomes/{id} (status) ``
  1. MUST emit 5 memory audit kinds per DEC-2294. PII per TASK-MEMORY-111: share counts SHA256.
  1. MUST thread trace_id from termination trigger → sign → commit → audit.
  1. MUST NOT commit without dual-sign per DEC-2292.
  1. MUST NOT mutate committed per DEC-2293.
  1. MUST be idempotent per UNIQUE(termination_id, grant_id) — one outcome per (termination, grant) pair.

§2 — Why this design

Why 5 outcomes (DEC-2291)? Captures full spectrum — voluntary (GL full), redundancy (GL pro-rated), misconduct (BL forfeit), severe (BL full), negotiated.

Why dual-sign (DEC-2292)? Equity decisions = high-stakes; CFO budget + CEO governance.

Why immutable (DEC-2293)? Member trust depends on irrevocability; corrections require board oversight.


§3 — API contract

Sample outcome:

{
  "outcome_id": "uuid",
  "termination_id": "uuid",
  "grant_id": "uuid",
  "outcome": "good_leaver_full_vest",
  "shares_vested_at_term": 2500,
  "shares_forfeited": 0,
  "shares_retained": 2500,
  "status": "committed"
}

§4 — Acceptance criteria

  1. leaver_outcome enum cardinality 5. 2. Triggered by TASK-HR-009. 3. CFO+CEO dual-sign. 4. Same-person rejected. 5. GL full → vesting halt at term. 6. BL unvested → vested retained. 7. BL full → all forfeited. 8. UNIQUE(termination_id, grant_id). 9. 5 memory audit kinds emitted. 10. PII scrubbed (shares SHA256). 11. RLS denies cross-tenant. 12. Trace_id preserved. 13. Append-only via REVOKE except status cols. 14. bigint shares. 15. shares math: vested+forfeit+retained ≤ grant.total. 16. CFO/CEO-only sign. 17. Immutable post-commit. 18. Grant status → cancelled_unvested or fully_vested per outcome. 19. Notes field for mutual_negotiated. 20. Auto-draft on termination execute.

§5 — Verification

#[tokio::test]
async fn dual_sign_required_to_commit() {
    let ctx = TestContext::with_drafted_outcome().await;
    ctx.cfo_sign(ctx.outcome_id).await;
    let o = ctx.fetch_outcome(ctx.outcome_id).await;
    assert_ne!(o.status, "committed");
    ctx.ceo_sign(ctx.outcome_id).await;
    let o2 = ctx.fetch_outcome(ctx.outcome_id).await;
    assert_eq!(o2.status, "committed");
}

#[tokio::test]
async fn gl_full_vest_at_term() {
    let ctx = TestContext::with_member_grant_3y_vested_5000().await;
    ctx.terminate_gl(ctx.member_id).await;
    ctx.cfo_sign_outcome(...).await;
    ctx.ceo_sign_outcome(...).await;
    let o = ctx.fetch_outcome(...).await;
    assert_eq!(o.shares_retained, 5000);
    assert_eq!(o.shares_forfeited, 0);
}

#[tokio::test]
async fn bl_full_forfeit() {
    let ctx = TestContext::with_member_grant_vested_5000_total_10000().await;
    ctx.set_outcome(..., "bad_leaver_full_forfeit").await;
    ctx.both_sign(...).await;
    let o = ctx.fetch_outcome(...).await;
    assert_eq!(o.shares_retained, 0);
    assert_eq!(o.shares_forfeited, 10000);
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-HR-009. Cross-module: TASK-ESOP-001 (grant), TASK-ESOP-002 (vested calc), TASK-AUTH-101 (CFO/CEO), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
One signer missinggatereject commitwait
Same-personvalidate403different signer
Math invariant violationcheck vested+forfeit+retainedsev-1; rejectbug fix
Cross-tenantRLS403inherent
Duplicate outcomeUNIQUE409inherent
HR-009 termination not executedcheck412wait
Mutual_negotiated without notesvalidatewarn; acceptprovide notes
Concurrent signUPDATE WHEREfirst winsinherent
Grant doesn't existFK404inherent
Decimal precisionbigintinherentinherent

§11 — Implementation notes


End of TASK-ESOP-005 spec.