tidaldb/.sdlc/features/m10-community-policy-engine/tasks.md

104 lines
5.8 KiB
Markdown

# Tasks: Community Policy Engine
## T1 — Define `CommunityPolicy`, `CommunityContext`, and schema types
**File:** `tidal/src/schema/validation/community_policy.rs` (new)
- Define `CommunityPolicy` struct with `allowed_write_signals`, `denied_write_signals`, `allowed_read_signals`, `denied_read_signals` fields (all `Vec<String>`).
- Define `CommunityContext` struct with `community_id: String` and `role: String`.
- Define `pub(super) CommunityPolicyEntry` with `name: String` and `policy: CommunityPolicy`.
- Add 4 new `SchemaError` variants to `tidal/src/schema/error.rs`: `InvalidCommunityPolicyName(String)`, `DuplicateCommunityPolicyName(String)`, `CommunityPolicySignalNotInSchema { policy: String, signal: String }`, `CommunityPolicySignalConflict { policy: String, signal: String }`.
- Re-export `CommunityPolicy` and `CommunityContext` from `schema/validation/mod.rs` and `schema/mod.rs`.
- Unit tests: struct construction, field access.
**Acceptance:** `cargo test --lib -p tidaldb` passes; `clippy -D warnings` clean.
---
## T2 — Add `CommunityPolicy` to `Schema` and `SchemaBuilder`
**Files:** `tidal/src/schema/validation/builders.rs`, `tidal/src/schema/validation/mod.rs`
- Add `community_policies: Vec<CommunityPolicyEntry>` field to `SchemaBuilder`.
- Implement `SchemaBuilder::community_policy(name: &str, policy: CommunityPolicy) -> &mut Self`.
- Add full validation in `SchemaBuilder::build()`:
- Name must be valid identifier (reuse `is_valid_signal_name`).
- No duplicate names.
- All signal references must exist in schema signals.
- No write allow/deny conflict.
- No read allow/deny conflict.
- Add `community_policies: HashMap<String, CommunityPolicy>` field to `Schema`.
- Implement `Schema::community_policy(role: &str) -> Option<&CommunityPolicy>`.
- Implement `Schema::community_policy_count() -> usize`.
- Unit tests: valid schema with community policies; each rejection case for new `SchemaError` variants.
**Acceptance:** All schema validation tests pass; `clippy -D warnings` clean.
---
## T3 — Implement `CommunityPolicyEvaluator` and `signal_with_community_policy()`
**Files:** `tidal/src/db/community.rs`, `tidal/src/session/policy.rs`
- Implement `CommunityPolicyEvaluator<'a>` with `policy: &'a CommunityPolicy` and `role: &'a str`.
- Implement `check_write(&self, signal_type: &str) -> Result<(), PolicyViolation>` using deny-first, allow-list logic.
- Add `CommunityWriteDenied` and `CommunityWriteNotAllowed` variants to `PolicyViolationKind` in `tidal/src/session/policy.rs`.
- Implement `TidalDb::signal_with_community_policy(signal_type, entity_id, weight, timestamp, ctx: CommunityContext) -> crate::Result<()>`:
- Resolve policy from schema by `ctx.role`; return `TidalError::NotFound` if missing.
- Call `CommunityPolicyEvaluator::check_write`; map `PolicyViolation` to `TidalError::PolicyViolation`.
- On success, delegate to `self.signal(...)`.
- Unit tests for `check_write`: allow path, deny-list path, allow-list miss path.
**Acceptance:** `cargo test --lib` passes; `clippy -D warnings` clean.
---
## T4 — Thread `CommunityContext` through retrieval and scoring
**Files:** `tidal/src/query/retrieve/types.rs`, `tidal/src/ranking/executor/context.rs`, scoring loop files
- Add `community: Option<CommunityContext>` to `Retrieve` struct.
- Add `community(ctx: CommunityContext) -> Self` to `RetrieveBuilder`.
- Add `community: Option<CommunityContext>` to `ExecutorContext` (or equivalent context struct).
- In `ProfileExecutor::score_candidates` (or equivalent scoring entry point):
- If `community` present: resolve `CommunityPolicy` from schema; return `QueryError::NotFound` if role missing.
- Build `suppressed: HashSet<SignalTypeId>` from read deny/allow lists using `schema.resolve_signal_type()`.
- In the scoring loop: skip signal contributions whose `SignalTypeId` is in `suppressed`.
- Fast path: if `suppressed.is_empty()`, skip the `contains` check.
- Unit tests: suppressed signals excluded from score; non-suppressed signals included; empty suppressed set takes fast path.
**Acceptance:** `cargo test --lib` passes; `clippy -D warnings` clean.
---
## T5 — Integration tests (`m10_community_policy.rs`)
**File:** `tidal/tests/m10_community_policy.rs` (new)
Cover all 9 scenarios from the spec test matrix:
1. Write allowed signal under member role → success.
2. Write denied signal under member role → `PolicyViolation(CommunityWriteDenied)`.
3. Write signal not in allow list under member role → `PolicyViolation(CommunityWriteNotAllowed)`.
4. Write any signal under admin role (empty allow list, empty deny list) → success.
5. Read suppressed signal excluded from ranking score: two candidates with equal base signals; one has a suppressed signal contribution; suppressed candidate ranks lower.
6. Read suppressed signal with no community context on query → score component included normally.
7. Schema with duplicate policy name → `SchemaError::DuplicateCommunityPolicyName`.
8. Schema with unknown signal in policy → `SchemaError::CommunityPolicySignalNotInSchema`.
9. Schema with allow/deny conflict → `SchemaError::CommunityPolicySignalConflict`.
10. Query with unknown role name → `TidalError::NotFound` (or `QueryError::NotFound`).
**Acceptance:** All 10 integration tests pass with `cargo test --test m10_community_policy`; zero test failures.
---
## T6 — Final polish and verification
- Run `cargo test --manifest-path tidal/Cargo.toml --lib` → all lib tests pass.
- Run `cargo test --manifest-path tidal/Cargo.toml --test m10_community_policy` → all integration tests pass.
- Run `cargo clippy --manifest-path tidal/Cargo.toml -- -D warnings` → zero warnings.
- Run `cargo fmt --manifest-path tidal/Cargo.toml --check` → no format issues.
- Confirm existing test suites unaffected: `m5_uat`, `m6_uat`, `m7_uat`, `m8_uat`.
**Acceptance:** All checks green; no regressions in any existing test suite.