# 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`). - 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` 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` 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` to `Retrieve` struct. - Add `community(ctx: CommunityContext) -> Self` to `RetrieveBuilder`. - Add `community: Option` 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` 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.