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

5.8 KiB

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.