Task — engineering-spec@1

"OKR 3 KR types — hit_target + improvement + milestone with type-specific progress calculation"

draftTASK-OKR-002
module okr · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-OKR-001 · blocks none

§1 — Description (BCP-14 normative)

The OKR service MUST extend KR schema with 3 types at services/okr/src/kr_type/ enforcing per-type validation + deterministic progress, 3 memory audit kinds.

  1. MUST validate kr_type against closed enum per DEC-1971.
  1. MUST define schema extension at migration 0002: ``sql ALTER TABLE okr_krs ADD COLUMN kr_type TEXT NOT NULL DEFAULT 'improvement' CHECK (kr_type IN ('hit_target','improvement','milestone')); ALTER TABLE okr_krs ADD COLUMN start_value NUMERIC(18,4); ALTER TABLE okr_krs ADD COLUMN target_value NUMERIC(18,4); ALTER TABLE okr_krs ADD COLUMN current_value NUMERIC(18,4); ALTER TABLE okr_krs ADD COLUMN milestone_checkpoints JSONB; ALTER TABLE okr_krs ADD COLUMN computed_progress_pct NUMERIC(5,2) CHECK (computed_progress_pct IS NULL OR (computed_progress_pct >= 0 AND computed_progress_pct <= 100)); GRANT UPDATE (kr_type, start_value, target_value, current_value, milestone_checkpoints, computed_progress_pct) ON okr_krs TO cyberos_app; ``
  1. MUST validate per-type at validator.rs::validate(kr) per DEC-1973:
  1. MUST compute progress at progress_calc.rs::compute(kr) → NUMERIC(5,2) per DEC-1972:
  1. MUST be deterministic per DEC-1972 — pure function, same inputs → same output.
  1. MUST emit 3 memory audit kinds per DEC-1974. PII per TASK-MEMORY-111: KR text hashed; type + progress ok.
  1. MUST thread trace_id from set/compute → audit.
  1. MUST NOT skip validation per DEC-1973 (reject 400 on wrong-type config).
  1. MUST NOT use non-deterministic progress (no now(), no random).

§2 — Why this design

Why 3 types (DEC-1970)? Covers all KR patterns; closed set prevents type sprawl. Industry standard (Doerr OKR formulation).

Why per-type fields (DEC-1973)? Type-specific schema prevents confusion (no checkpoints on improvement KRs).

Why deterministic (DEC-1972)? Weekly check-in numbers must be reproducible — auditors expect "same data = same number".

Why JSONB for milestones (DEC-1973)? Variable-length array; simpler than separate table.


§3 — API contract

Sample KR with type:

{
  "kr_id": "uuid",
  "title": "Increase MRR from $100k to $150k",
  "kr_type": "improvement",
  "start_value": 100000,
  "target_value": 150000,
  "current_value": 125000,
  "computed_progress_pct": 50.00
}

Sample milestone KR:

{
  "kr_type": "milestone",
  "milestone_checkpoints": [
    {"name": "Sign 3 design partners", "completed": true},
    {"name": "Ship MVP", "completed": true},
    {"name": "First $10k revenue", "completed": false}
  ],
  "computed_progress_pct": 66.67
}

§4 — Acceptance criteria

  1. kr_type enum cardinality 3. 2. hit_target requires target_value. 3. improvement requires start+target. 4. milestone requires checkpoints. 5. Wrong-type fields rejected (sev-3 audit). 6. hit_target progress 0 or 100. 7. improvement progress capped 0-100. 8. milestone progress = completed/total ratio. 9. 3 memory audit kinds emitted. 10. PII scrubbed (KR text SHA256). 11. RLS denies cross-tenant. 12. Trace_id preserved. 13. Deterministic (pure function). 14. rust_decimal precision. 15. CHECK constraint on progress 0-100. 16. Append-only via REVOKE except 6 cols. 17. Type change requires new KR (immutable type). 18. Empty checkpoints array → progress=0. 19. Start = Target rejected (improvement). 20. Negative progress impossible (clamp).

§5 — Verification

#[tokio::test]
async fn improvement_progress_50pct() {
    let kr = mk_kr_improvement(100, 200, 150);
    assert_eq!(progress_calc::compute(&kr), dec!(50.00));
}

#[tokio::test]
async fn hit_target_binary() {
    let kr = mk_kr_hit_target(100, 99);
    assert_eq!(progress_calc::compute(&kr), dec!(0));
    let kr2 = mk_kr_hit_target(100, 100);
    assert_eq!(progress_calc::compute(&kr2), dec!(100));
}

#[tokio::test]
async fn milestone_validation_requires_checkpoints() {
    let r = create_kr_milestone_without_checkpoints().await;
    assert!(r.is_err());
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-OKR-001. Downstream: TASK-OKR-003 (auto-progress reads computed_progress_pct). Cross-module: TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Wrong-type fields setvalidator400use right type
Invalid kr_typeCHECK400use valid enum
Progress > 100clamp100 maxinherent
Progress < 0clamp0 mininherent
Improvement start == targetreject400non-degenerate
Milestone empty checkpointsprogress=0inherentinherent
Decimal precision driftrust_decimalinherentinherent
Type changereject (immutable)409new KR
Cross-tenant writeRLS403inherent
Concurrent progress updatelast-writer-winsinherentinherent

§11 — Implementation notes


End of TASK-OKR-002 spec.