Task — engineering-spec@1

"LEARN VP (Voting Power) deterministic nightly roll-up — aggregates PROJ + TIME + KB contributions into per-Member VP score"

draftTASK-LEARN-003
module learn · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-PROJ-013, TASK-TIME-001 · blocks TASK-LEARN-007

§1 — Description (BCP-14 normative)

The LEARN service MUST ship VP rollup at services/learn/src/vp/ aggregating PROJ + TIME + KB nightly, immutable per-week snapshots, versioned weights, 4 memory audit kinds.

  1. MUST validate vp_component against closed enum per DEC-2101.
  1. MUST aggregate at aggregator.rs::aggregate(member, week, weights):
  1. MUST be deterministic per DEC-2103 — pure function; same data + same weights → same output.
  1. MUST schedule nightly batch via TASK-MCP-007 at 03:30 tenant_tz.
  1. MUST define tables at migration 0003: ```sql CREATE TABLE learn_vp_weights ( tenant_id UUID NOT NULL, version INT NOT NULL, weight_proj NUMERIC(5,4) NOT NULL, weight_time NUMERIC(5,4) NOT NULL, weight_kb NUMERIC(5,4) NOT NULL, effective_from DATE NOT NULL, set_by UUID NOT NULL, PRIMARY KEY (tenant_id, version) ); ALTER TABLE learn_vp_weights ENABLE ROW LEVEL SECURITY; CREATE POLICY weights_rls ON learn_vp_weights USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON learn_vp_weights FROM cyberos_app;

CREATE TABLE learn_vp_snapshots ( snapshot_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, member_id UUID NOT NULL, iso_week CHAR(8) NOT NULL, proj_score NUMERIC(10,4) NOT NULL, time_score NUMERIC(10,4) NOT NULL, kb_score NUMERIC(10,4) NOT NULL, total_vp NUMERIC(10,4) NOT NULL, weights_version INT NOT NULL, correction_of UUID REFERENCES learn_vp_snapshots(snapshot_id), trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, member_id, iso_week, weights_version) ); CREATE INDEX vp_snap_member_idx ON learn_vp_snapshots(tenant_id, member_id, iso_week DESC); ALTER TABLE learn_vp_snapshots ENABLE ROW LEVEL SECURITY; CREATE POLICY vp_snap_rls ON learn_vp_snapshots USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON learn_vp_snapshots FROM cyberos_app; ```

  1. MUST expose endpoints: ``text POST /v1/learn/vp/weights (CEO; new version) GET /v1/learn/vp/weights (latest) GET /v1/learn/members/{id}/vp (history) POST /v1/learn/vp/rollup/trigger (CEO manual) ``
  1. MUST emit 4 memory audit kinds per DEC-2104. PII per TASK-MEMORY-111: scores SHA256.
  1. MUST thread trace_id from cron → aggregator → audit.
  1. MUST NOT mutate prior snapshot per DEC-2102 (correction = new row).
  1. MUST NOT use non-deterministic inputs (no now(), no random).

§2 — Why this design

Why deterministic (DEC-2103)? VP drives governance + REW BP fund distribution; replay must yield same result for audit.

Why versioned weights (DEC-2103)? Weights change over time; snapshots must record which version applied.

Why immutable snapshots (DEC-2102)? Historical VP is institutional record; rewriting = governance failure.

Why 3 components (DEC-2101)? Captures core contribution dimensions; closed enum prevents add-hoc scope creep.


§3 — API contract

Sample VP snapshot:

{
  "member_id": "uuid",
  "iso_week": "2026-W20",
  "proj_score": 45.5,
  "time_score": 32.0,
  "kb_score": 8.5,
  "total_vp": 86.0,
  "weights_version": 3
}

§4 — Acceptance criteria

  1. vp_component enum cardinality 3. 2. Deterministic (same input → same output). 3. Snapshots immutable. 4. Weights versioned. 5. Nightly cron 03:30. 6. UNIQUE(tenant, member, week, weights_version) idempotency. 7. 4 memory audit kinds emitted. 8. PII scrubbed (scores SHA256). 9. RLS denies cross-tenant. 10. CEO-only weights + manual trigger. 11. Trace_id preserved. 12. rust_decimal precision (10,4). 13. Correction via new snapshot with correction_of link. 14. Append-only via REVOKE. 15. Inactive member skipped. 16. 0 active members skipped. 17. Weights version pinning enforced. 18. Backfill via manual trigger with iso_week. 19. History query desc time. 20. Per-component score visible.

§5 — Verification

#[tokio::test]
async fn deterministic_replay() {
    let ctx = TestContext::with_member_data().await;
    let v1 = ctx.run_rollup(this_week()).await;
    ctx.run_rollup(this_week()).await;
    let v2 = ctx.fetch_snapshot(ctx.member_id, this_week()).await;
    assert_eq!(v1.total_vp, v2.total_vp);
}

#[tokio::test]
async fn snapshot_immutable() {
    let ctx = TestContext::with_vp_snapshot().await;
    let r = ctx.try_mutate_snapshot(ctx.snapshot_id).await;
    assert!(r.is_err());
}

#[tokio::test]
async fn correction_via_new_row() {
    let ctx = TestContext::with_vp_snapshot().await;
    ctx.run_correction_with_new_weights(ctx.snapshot_id).await;
    let snaps = ctx.fetch_snapshots_for_week(ctx.iso_week).await;
    assert_eq!(snaps.len(), 2);
    let corrected = snaps.iter().find(|s| s.correction_of.is_some()).unwrap();
    assert_eq!(corrected.correction_of, Some(ctx.snapshot_id));
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-PROJ-013, TASK-TIME-001. Cross-module: TASK-LEARN-001 (member context), TASK-KB-001 (doc authorship), TASK-MCP-007 (cron), TASK-AUTH-101 (CEO), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Source module failcatchsev-2; skip componentretry
Weights missingcatchsev-1; haltseed weights
Cron skippedcatch-upinherentinherent
Duplicate snapshotUNIQUEskipinherent
Decimal precision driftrust_decimalinherentinherent
Non-deterministic inputcode reviewinherentbug fix
Cross-tenant queryRLS0 rowsinherent
Mid-rollup crashresumepartialretry
Weights mid-week changeuse snapshot's weights_versioninherentinherent
Inactive memberskipinherentinherent

§11 — Implementation notes


End of TASK-LEARN-003 spec.