"OKR 3 KR types — hit_target + improvement + milestone with type-specific progress calculation"
§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.
- MUST validate
kr_typeagainst closed enum per DEC-1971.
- 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;``
- MUST validate per-type at
validator.rs::validate(kr)per DEC-1973:
- hit_target → target_value REQUIRED; others NULL
- improvement → start_value + target_value REQUIRED; checkpoints NULL
- milestone → checkpoints REQUIRED (array of
{name, completed: bool}); start/target NULL
- MUST compute progress at
progress_calc.rs::compute(kr) → NUMERIC(5,2)per DEC-1972:
- hit_target:
current_value >= target_value ? 100 : 0 - improvement:
((current - start) / (target - start)) * 100, capped 0-100 - milestone:
(completed_count / total_count) * 100
- MUST be deterministic per DEC-1972 — pure function, same inputs → same output.
- MUST emit 3 memory audit kinds per DEC-1974. PII per TASK-MEMORY-111: KR text hashed; type + progress ok.
- MUST thread trace_id from set/compute → audit.
- MUST NOT skip validation per DEC-1973 (reject 400 on wrong-type config).
- 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
- 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
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| Wrong-type fields set | validator | 400 | use right type |
| Invalid kr_type | CHECK | 400 | use valid enum |
| Progress > 100 | clamp | 100 max | inherent |
| Progress < 0 | clamp | 0 min | inherent |
| Improvement start == target | reject | 400 | non-degenerate |
| Milestone empty checkpoints | progress=0 | inherent | inherent |
| Decimal precision drift | rust_decimal | inherent | inherent |
| Type change | reject (immutable) | 409 | new KR |
| Cross-tenant write | RLS | 403 | inherent |
| Concurrent progress update | last-writer-wins | inherent | inherent |
§11 — Implementation notes
- §11.1 progress_calc pure:
(kr_type, fields) → Decimal. - §11.2 Computed_progress_pct cached on row; updated on current_value change.
- §11.3 memory audit body: kr_id, kr_type, progress; title SHA256.
- §11.4 Milestone JSONB schema:
[{name: str, completed: bool, completed_at?: timestamp}]. - §11.5 Future kr types via enum addition (e.g. "burndown") + new calc branch.
End of TASK-OKR-002 spec.