tidaldb/docs/planning/milestone-10/phase-1/OVERVIEW.md
jx12n ad4134e280 chore: doc consolidation, seven-dimension review fixes, and commit hooks
- Eliminate the tidal/ self-contained doc mirror; docs now have two canonical
  homes (root *.md and docs/), with planning/specs/research/reviews moved up
- Remove stale .agents/skills and .ai mirrors; canonicalize skills under .claude/
- Add pre-commit hook + scripts/check-docs.sh doc-guard + scripts/install-hooks.sh
- Implement M0-M10 seven-dimension review findings across engine, net, server,
  and tidalctl (durability, replication, query, WAL, storage, CLI hardening)
2026-06-08 22:46:28 -06:00

597 lines
27 KiB
Markdown

# m10p1: Community Governance Policy Engine
## Delivers
The versioned, schema-declared governance policy layer that lets a community
decide *which* signal intents may influence ranking, *within what weighting
bounds*, and *above what trust/quality threshold* -- and makes the governing
policy version visible in every query result for explainability. After this
phase a community can declare a `GovernancePolicy` in the schema, register
successive versions with monotonic version numbers and effective timestamps,
and out-of-policy community-scoped signals are rejected or quarantined at the
write path instead of silently contaminating the community aggregate. The
runtime threads the active `policy_id`/`policy_version` from scoring into
`Results.policy_metadata`, so callers can attribute a ranked feed to the exact
governance version that produced it.
This is the "rules first" phase of M10: no agent capability enforcement yet
(M10p2), no provenance graph or remove-by-scope (M10p3). It builds the policy
*registry and contract* on top of the M9 scope/share primitives, which are
treated as GIVEN.
Deliverables:
- `GovernancePolicy { community_id, version, effective_at_ns, allowed_intents, excluded_intents, weighting_bounds, trust_threshold }` in `governance/policy.rs`
- `WeightingBounds { min_weight, max_weight, per_intent }` newtype with clamp + in-bounds checks
- `GovernanceRegistry`: versioned `BTreeMap<u32, GovernancePolicy>` per `CommunityId` (mirrors `ProfileRegistry` name->version->profile structure and monotonic-version validation)
- `SchemaBuilder::community_policy(community_id, GovernancePolicy)` declaration, validated at `build()` time (monotonic versions, allowed/excluded disjoint, bounds well-formed)
- `policy_version` threaded from scoring into `Results.policy_metadata` and `QueryStats`
- Out-of-policy community-scoped signal handling: reject (`TidalError`) or route to a quarantine ledger, enforced synchronously in `signal_with_context` / `try_cohort_attribution`
- All existing M0-M9 tests pass unchanged (no community policy declared = no governance enforcement; `policy_metadata` is `None`)
## Dependencies
- **Requires:** M9 complete -- `SignalScope`, `CommunityId`, `SignalProvenance`, `Membership`/`MembershipEpoch`, `SharePolicy` (all in `tidal/src/governance/`), WAL V3 event envelope carrying `scope`, and the community signal dispatch path in `db/signals.rs`. These M9 primitives are GIVEN and are extended here, never redefined.
- **Files modified:**
- `tidal/src/governance/mod.rs` -- export `policy::{GovernancePolicy, WeightingBounds, GovernanceRegistry, GovernanceError, PolicyMetadata, IntentDisposition}`
- `tidal/src/schema/validation/builders.rs` -- `SchemaBuilder::community_policy`, a `gov_policies: Vec<GovPolicyEntry>` field, and policy validation in `build()`
- `tidal/src/schema/validation/mod.rs` -- `Schema` carries `governance: HashMap<CommunityId, BTreeMap<u32, GovernancePolicy>>`; accessor `governance_policies()`
- `tidal/src/schema/error.rs` -- `SchemaError` governance variants (non-monotonic version, intent in both lists, bad bounds, duplicate effective timestamp)
- `tidal/src/ranking/executor/mod.rs` -- `score_inner` accepts an optional active `PolicyMetadata` and stamps it onto results (no scoring math change)
- `tidal/src/query/retrieve/types.rs` -- `Results.policy_metadata: Option<PolicyMetadata>`
- `tidal/src/query/stats.rs` -- `QueryStats.policy_version: Option<u32>` governance telemetry
- `tidal/src/db/signals.rs` -- out-of-policy gate in `signal_with_context` (community scope) and `try_cohort_attribution`
- `tidal/src/db/open.rs` -- load community governance policies into a `GovernanceRegistry` after `with_schema`, store on `TidalDb`
- `tidal/src/db/mod.rs` -- `governance_registry: Arc<GovernanceRegistry>` field + quarantine ledger handle
- **Files created:**
- `tidal/src/governance/policy.rs` -- `GovernancePolicy`, `WeightingBounds`, `GovernanceRegistry`, `GovernanceError`, `PolicyMetadata`, `IntentDisposition`
## Research References
- `docs/research/tidaldb_ranking.md` -- ranking profile versioning model (the `ProfileRegistry` name->version->profile pattern this registry mirrors)
- `/tmp/m9m10_brief.md` sections 1.6, 2 (M10p1 row), 4, 5 -- canonical primitive shapes and invariants
- `tidal/src/ranking/registry.rs:92,187` -- `ProfileRegistry` versioned `BTreeMap` + monotonic-version validation (direct structural precedent)
- `thoughts.md` -- versioned schema artifacts and effective-timestamp semantics
## Acceptance Criteria (Phase Level)
- [ ] `GovernancePolicy` declares `allowed_intents: Vec<String>`, `excluded_intents: Vec<String>`, and `weighting_bounds: WeightingBounds` (min/max global + optional per-intent overrides); `WeightingBounds::clamp(intent, w)` returns the in-bounds weight and `in_bounds(intent, w)` is a pure predicate
- [ ] `GovernancePolicy` carries `version: u32` and `effective_at_ns: u64`; `GovernanceRegistry::register` rejects a version `<=` the current max for that `CommunityId` with `GovernanceError::VersionConflict` and rejects an `effective_at_ns` not strictly greater than the prior version's
- [ ] `GovernanceRegistry::active_at(community_id, now_ns)` returns the highest-version policy whose `effective_at_ns <= now_ns` (or `None`), in O(log n) over the per-community `BTreeMap`
- [ ] `SchemaBuilder::community_policy(community_id, policy)` declares a policy; `build()` validates monotonic versions, disjoint allowed/excluded intent sets, and well-formed bounds (`min_weight <= max_weight`, finite), returning a `SchemaError` on violation
- [ ] A RETRIEVE query against a community-scoped profile returns `Results.policy_metadata = Some(PolicyMetadata { community_id, policy_version, effective_at_ns })` and `QueryStats.policy_version = Some(version)`; with no governance policy declared both are `None` and all prior tests pass unchanged
- [ ] A community-scoped signal whose intent is in `excluded_intents` (or not in a non-empty `allowed_intents`) is rejected with `TidalError` OR routed to the quarantine ledger per `IntentDisposition`, and NEVER reaches the community aggregate -- verified by asserting the community ledger count is unchanged
- [ ] `Local`-scope signals are never subject to governance gating (local-profile-intact guarantee): a local signal with a community-disallowed intent still records to the local ledger
- [ ] The out-of-policy gate is a synchronous O(1)/O(log n) read on the write path -- no dependency on the 60s sweeper
- [ ] Property test: 10,000 random `(intent, weight)` pairs -- `clamp` output is always within `[min, max]` for that intent and `in_bounds` agrees with `clamp(w) == w`
- [ ] `cargo clippy -p tidaldb -D warnings` and `cargo fmt` pass; `cargo test -p tidaldb --lib` green
## Task Execution Order
```
Task 01: Policy Types ──────────────┐
(GovernancePolicy, WeightingBounds, │
PolicyMetadata, GovernanceError) │
├──> Task 03: Schema Declaration
Task 02: GovernanceRegistry ─────────┤ (SchemaBuilder::community_policy
(versioned BTreeMap, register, │ + build() validation + Schema accessor)
active_at, monotonic validation) │ │
│ v
│ Task 04: Open-Path Wiring
│ (db/open.rs load registry,
│ db/mod.rs field + quarantine ledger)
│ │
│ v
├──> Task 05: Result Metadata Threading
│ (executor -> policy_metadata,
│ QueryStats.policy_version)
│ │
│ v
└──> Task 06: Write-Path Enforcement
(signal_with_context +
try_cohort_attribution gate,
quarantine routing)
```
Tasks 01 and 02 are parallelizable (02 depends only on the 01 types). Task 03
depends on 01+02. Task 04 depends on 03. Task 05 depends on 01 (the metadata
type) and 04 (the registry on `TidalDb`). Task 06 depends on 04 (registry +
quarantine ledger handles) and is the integration capstone.
## Module Location
| File | Status | Contains |
|------|--------|----------|
| `tidal/src/governance/policy.rs` | NEW | `GovernancePolicy`, `WeightingBounds`, `GovernanceRegistry`, `GovernanceError`, `PolicyMetadata`, `IntentDisposition` |
| `tidal/src/governance/mod.rs` | MODIFIED | Re-export policy types alongside M9 scope/provenance/share_policy/membership |
| `tidal/src/schema/validation/builders.rs` | MODIFIED | `SchemaBuilder::community_policy`, `gov_policies` field, `build()` validation |
| `tidal/src/schema/validation/mod.rs` | MODIFIED | `Schema.governance` map + `governance_policies()` accessor |
| `tidal/src/schema/error.rs` | MODIFIED | `SchemaError` governance variants |
| `tidal/src/ranking/executor/mod.rs` | MODIFIED | Stamp active `PolicyMetadata` onto scored results (no math change) |
| `tidal/src/query/retrieve/types.rs` | MODIFIED | `Results.policy_metadata: Option<PolicyMetadata>` |
| `tidal/src/query/stats.rs` | MODIFIED | `QueryStats.policy_version: Option<u32>` |
| `tidal/src/db/signals.rs` | MODIFIED | Out-of-policy community-scope gate + quarantine routing |
| `tidal/src/db/open.rs` | MODIFIED | Build `GovernanceRegistry` from schema after `with_schema` |
| `tidal/src/db/mod.rs` | MODIFIED | `governance_registry` + quarantine ledger fields on `TidalDb` |
## Technical Design
All types live in `tidal/src/governance/policy.rs`. `CommunityId` and
`SignalScope` are imported from the M9 `governance::scope` module -- not
redefined here.
### WeightingBounds
```rust
// tidal/src/governance/policy.rs
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::governance::scope::CommunityId;
/// Per-intent weighting constraints a governance policy enforces on
/// community-scoped signals.
///
/// `min_weight`/`max_weight` are the global bounds; `per_intent` overrides
/// them for named intents (e.g. clamp `low_quality` harder than `share`).
/// All weights are signal contribution weights in the same space the ranking
/// engine consumes; bounds are inclusive and must be finite with
/// `min_weight <= max_weight`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WeightingBounds {
pub min_weight: f64,
pub max_weight: f64,
/// Intent name -> (min, max) override. Falls back to the global bounds.
pub per_intent: BTreeMap<String, (f64, f64)>,
}
impl WeightingBounds {
/// Permissive default: `[0.0, 1.0]`, no per-intent overrides.
#[must_use]
pub fn unbounded_unit() -> Self {
Self { min_weight: 0.0, max_weight: 1.0, per_intent: BTreeMap::new() }
}
/// Resolve the inclusive `(min, max)` bound for `intent`.
#[must_use]
pub fn bounds_for(&self, intent: &str) -> (f64, f64) {
self.per_intent
.get(intent)
.copied()
.unwrap_or((self.min_weight, self.max_weight))
}
/// Clamp `weight` into the bounds for `intent`. Pure; total ordering safe
/// because policy bounds are validated finite at schema build.
#[must_use]
pub fn clamp(&self, intent: &str, weight: f64) -> f64 {
let (lo, hi) = self.bounds_for(intent);
weight.max(lo).min(hi)
}
/// Whether `weight` already lies within the bounds for `intent`.
#[must_use]
pub fn in_bounds(&self, intent: &str, weight: f64) -> bool {
let (lo, hi) = self.bounds_for(intent);
(lo..=hi).contains(&weight)
}
/// Validate well-formedness: finite, `min <= max`, every override finite
/// and ordered.
pub(crate) fn validate(&self) -> Result<(), GovernanceError> {
if !self.min_weight.is_finite()
|| !self.max_weight.is_finite()
|| self.min_weight > self.max_weight
{
return Err(GovernanceError::BadBounds {
min: self.min_weight,
max: self.max_weight,
});
}
for (intent, (lo, hi)) in &self.per_intent {
if !lo.is_finite() || !hi.is_finite() || lo > hi {
return Err(GovernanceError::BadIntentBounds {
intent: intent.clone(),
min: *lo,
max: *hi,
});
}
}
Ok(())
}
}
```
### GovernancePolicy
```rust
// tidal/src/governance/policy.rs
/// How a community treats a signal whose intent is outside the policy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum IntentDisposition {
/// Reject the write with a `TidalError` (loud, default).
#[default]
Reject,
/// Record into the quarantine ledger instead of the community aggregate.
Quarantine,
}
/// A versioned community governance policy.
///
/// Declared in the schema via [`SchemaBuilder::community_policy`] and resolved
/// at runtime through the [`GovernanceRegistry`]. A policy governs which signal
/// intents may influence the community aggregate (`allowed_intents` /
/// `excluded_intents`), the weighting bounds applied to admitted signals
/// (`weighting_bounds`), and the minimum trust/quality threshold a contributor
/// must meet (`trust_threshold`). Versions are monotonic per `community_id`;
/// `effective_at_ns` selects the active version for a given query time.
///
/// Only community-scoped signals are governed. `SignalScope::Local` events are
/// never subject to this policy -- the local-profile-intact guarantee.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GovernancePolicy {
pub community_id: CommunityId,
/// Monotonic version, strictly increasing per `community_id`.
pub version: u32,
/// Wall-clock nanoseconds at which this version becomes active.
pub effective_at_ns: u64,
/// If non-empty, only these intents may influence the community aggregate.
/// Empty means "all intents not in `excluded_intents`".
pub allowed_intents: Vec<String>,
/// Intents always excluded, regardless of `allowed_intents`.
pub excluded_intents: Vec<String>,
/// Weighting constraints applied to admitted signals.
pub weighting_bounds: WeightingBounds,
/// Minimum contributor trust/quality score in `[0.0, 1.0]`; `0.0` admits all.
pub trust_threshold: f64,
/// Disposition for out-of-policy intents.
pub disposition: IntentDisposition,
}
impl GovernancePolicy {
/// Whether `intent` is admitted by this policy's allow/exclude lists.
///
/// Excluded always loses; an empty `allowed_intents` admits everything not
/// excluded; a non-empty `allowed_intents` requires membership.
#[must_use]
pub fn admits_intent(&self, intent: &str) -> bool {
if self.excluded_intents.iter().any(|i| i == intent) {
return false;
}
self.allowed_intents.is_empty()
|| self.allowed_intents.iter().any(|i| i == intent)
}
/// Validate internal consistency (disjoint lists, well-formed bounds,
/// trust threshold in range). Called at schema build.
pub(crate) fn validate(&self) -> Result<(), GovernanceError> {
for intent in &self.allowed_intents {
if self.excluded_intents.iter().any(|e| e == intent) {
return Err(GovernanceError::IntentInBothLists {
intent: intent.clone(),
});
}
}
if !(0.0..=1.0).contains(&self.trust_threshold) {
return Err(GovernanceError::BadTrustThreshold(self.trust_threshold));
}
self.weighting_bounds.validate()
}
}
```
### GovernanceRegistry
Mirrors `ProfileRegistry` (`tidal/src/ranking/registry.rs:92`): a per-key
versioned `BTreeMap` with monotonic-version validation on `register`.
```rust
// tidal/src/governance/policy.rs
/// Versioned registry of community governance policies.
///
/// Stores `community_id -> version -> policy`. `register` enforces monotonic
/// versions and strictly increasing `effective_at_ns` per community.
/// `active_at` selects the highest version whose `effective_at_ns <= now_ns`.
///
/// Mirrors `ProfileRegistry`'s structure so governance versioning behaves
/// identically to ranking-profile versioning.
#[derive(Debug, Default)]
pub struct GovernanceRegistry {
/// community_id -> version -> policy
policies: std::collections::HashMap<CommunityId, BTreeMap<u32, GovernancePolicy>>,
}
impl GovernanceRegistry {
#[must_use]
pub fn new() -> Self {
Self { policies: std::collections::HashMap::new() }
}
/// Register a policy version.
///
/// # Errors
/// - `VersionConflict` if `version <= max` for this community
/// - `NonMonotonicEffective` if `effective_at_ns <=` the prior version's
/// - any [`GovernancePolicy::validate`] error
pub fn register(&mut self, policy: GovernancePolicy) -> Result<(), GovernanceError> {
policy.validate()?;
let versions = self.policies.entry(policy.community_id).or_default();
if let Some((&max_v, prev)) = versions.last_key_value() {
if policy.version <= max_v {
return Err(GovernanceError::VersionConflict {
community_id: policy.community_id,
existing: max_v,
new: policy.version,
});
}
if policy.effective_at_ns <= prev.effective_at_ns {
return Err(GovernanceError::NonMonotonicEffective {
community_id: policy.community_id,
prev_effective_ns: prev.effective_at_ns,
new_effective_ns: policy.effective_at_ns,
});
}
}
versions.insert(policy.version, policy);
Ok(())
}
/// The policy active for `community_id` at `now_ns`: highest version whose
/// `effective_at_ns <= now_ns`. O(log n) over the per-community map.
#[must_use]
pub fn active_at(&self, community_id: CommunityId, now_ns: u64) -> Option<&GovernancePolicy> {
self.policies
.get(&community_id)?
.values()
.rev()
.find(|p| p.effective_at_ns <= now_ns)
}
/// The latest registered version for a community, ignoring effective time.
#[must_use]
pub fn latest(&self, community_id: CommunityId) -> Option<&GovernancePolicy> {
self.policies.get(&community_id)?.last_key_value().map(|(_, p)| p)
}
/// Whether any community has a policy declared.
#[must_use]
pub fn is_empty(&self) -> bool {
self.policies.is_empty()
}
}
```
### PolicyMetadata (result threading)
```rust
// tidal/src/governance/policy.rs
/// Governing-policy attribution attached to a query result for explainability.
///
/// Threaded from the active `GovernancePolicy` through the ranking executor
/// into `Results.policy_metadata`. `None` on results from a community with no
/// governance policy declared.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct PolicyMetadata {
pub community_id: CommunityId,
pub policy_version: u32,
pub effective_at_ns: u64,
}
impl From<&GovernancePolicy> for PolicyMetadata {
fn from(p: &GovernancePolicy) -> Self {
Self {
community_id: p.community_id,
policy_version: p.version,
effective_at_ns: p.effective_at_ns,
}
}
}
```
### GovernanceError
```rust
// tidal/src/governance/policy.rs
/// Errors from governance policy registration and validation.
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum GovernanceError {
#[error("community {community_id}: version {new} <= existing max {existing}")]
VersionConflict { community_id: CommunityId, existing: u32, new: u32 },
#[error(
"community {community_id}: effective_at {new_effective_ns} <= prior {prev_effective_ns}"
)]
NonMonotonicEffective {
community_id: CommunityId,
prev_effective_ns: u64,
new_effective_ns: u64,
},
#[error("intent '{intent}' appears in both allowed and excluded lists")]
IntentInBothLists { intent: String },
#[error("invalid weighting bounds: min {min} > max {max} or non-finite")]
BadBounds { min: f64, max: f64 },
#[error("intent '{intent}': invalid bounds min {min} > max {max} or non-finite")]
BadIntentBounds { intent: String, min: f64, max: f64 },
#[error("trust_threshold {0} out of range [0.0, 1.0]")]
BadTrustThreshold(f64),
}
```
### Schema declaration
`SchemaBuilder` gains a `gov_policies` vector and a `community_policy` method
that mirrors `session_policy` (`builders.rs:103`). At `build()`, governance
policies are folded into a `GovernanceRegistry`-shaped map on the `Schema`;
registry-level monotonic checks reuse `GovernanceRegistry::register` so the
build path and runtime path validate identically. `SchemaError` gains
`#[from]`-style governance variants surfacing `GovernanceError`.
```rust
// tidal/src/schema/validation/builders.rs (added)
/// Declare a versioned governance policy for a community.
///
/// Validated at `build()`: monotonic versions per community, disjoint
/// allowed/excluded intents, and well-formed weighting bounds. Declare after
/// signal declarations so intent names can be cross-checked where applicable.
pub fn community_policy(
&mut self,
community_id: CommunityId,
policy: GovernancePolicy,
) -> &mut Self {
self.gov_policies.push(GovPolicyEntry { community_id, policy });
self
}
```
```rust
// tidal/src/schema/validation/mod.rs (Schema gains a field + accessor)
impl Schema {
/// All governance policies, grouped `community_id -> version -> policy`.
#[must_use]
pub fn governance_policies(
&self,
) -> &HashMap<CommunityId, BTreeMap<u32, GovernancePolicy>> {
&self.governance
}
}
```
### Open-path wiring
```rust
// tidal/src/db/open.rs (in open_with_schema, after with_schema)
let mut governance_registry = GovernanceRegistry::new();
for versions in schema.governance_policies().values() {
for policy in versions.values() {
governance_registry.register(policy.clone()).map_err(|e| {
TidalError::internal("open", format!("invalid governance policy: {e}"))
})?;
}
}
// Stored as Arc<GovernanceRegistry> on TidalDb alongside profile_registry.
```
### Result metadata threading
The executor's `score_inner` does not change scoring math. The active
`PolicyMetadata` (resolved by the query layer via
`GovernanceRegistry::active_at(community_id, now)` for community-scoped
queries) is passed through and stamped onto the `Results`:
```rust
// tidal/src/query/retrieve/types.rs (Results gains a field)
pub struct Results {
// ... existing fields ...
/// Governing community policy version, for explainability. `None` when the
/// query is not community-scoped or no policy is declared.
pub policy_metadata: Option<crate::governance::policy::PolicyMetadata>,
}
// tidal/src/query/stats.rs (QueryStats gains a field)
pub struct QueryStats {
// ... existing fields ...
/// Governing policy version applied to this query, if any.
pub policy_version: Option<u32>,
}
```
### Write-path enforcement
The out-of-policy gate is added to `signal_with_context` (community scope) and
`try_cohort_attribution` (`db/signals.rs:219,280`). It is a synchronous read
of the active policy -- O(log n) registry lookup, no sweeper.
```rust
// tidal/src/db/signals.rs (sketch, inside the community-scope branch)
// Local scope is NEVER governed -- local-profile-intact guarantee.
if scope.is_community() {
if let Some(policy) = self
.governance_registry
.active_at(scope.community_id(), timestamp.as_nanos())
{
if !policy.admits_intent(signal_type) {
match policy.disposition {
IntentDisposition::Reject => {
return Err(TidalError::policy_rejected(signal_type));
}
IntentDisposition::Quarantine => {
self.quarantine_ledger.record(/* ... */);
return Ok(()); // never reaches the community aggregate
}
}
}
// Admitted: clamp the weight into policy bounds before community record.
weight = policy.weighting_bounds.clamp(signal_type, weight);
}
}
```
## Notes
### Local-profile-intact guarantee (load-bearing)
Governance gating runs ONLY for `SignalScope::Community`. A local-scope signal
with a community-disallowed intent must still record to the local ledger
unchanged. A routing bug that applies governance to local scope is silent data
loss on the user's own profile -- every test in this phase asserts the local
ledger is untouched by governance rejection/quarantine. (Brief sections 1.1, 4.5.)
### <1s enforcement is synchronous, never the sweeper
The out-of-policy gate and the active-policy lookup are O(log n) reads on the
write path. They must NOT route through the 60s sweeper (`db/sweeper.rs:14`).
Policy enforcement is immediate by construction. (Brief section 4.4.)
### Mirror ProfileRegistry, do not reinvent versioning
`GovernanceRegistry` deliberately copies `ProfileRegistry`'s `HashMap<key,
BTreeMap<u32, _>>` shape and monotonic-version rule (`registry.rs:92,187`).
Effective-timestamp ordering is the one addition: registration requires both
`version` and `effective_at_ns` to strictly increase, so `active_at` is an
unambiguous reverse scan. Do not introduce a parallel versioning scheme.
### M9 primitives are GIVEN -- never redefine
`CommunityId`, `SignalScope`, `SharePolicy`, `SignalProvenance`, `Membership`
are defined canonically in M9p1 (`tidal/src/governance/{scope,share_policy,
provenance,membership}.rs`). This phase imports them. `GovernancePolicy` is the
only new primitive; if its shape disagrees with the brief's section 1.6, the
brief wins. (Brief section 1.)
### Backward compatibility
No WAL/checkpoint format change in this phase -- `policy_version` is derived at
query time from the in-memory registry, not persisted per event (provenance
persistence is M10p3). A database with no `community_policy` declarations
behaves identically to M9: `policy_metadata`/`policy_version` are `None`,
governance gating is a no-op, and the `GovernanceRegistry` is empty. The new
`Results.policy_metadata`/`QueryStats.policy_version` fields are `Option`, so
existing construction sites set `None`. (Brief section 4.1.)
### Quarantine ledger
When `disposition = Quarantine`, the out-of-policy signal is recorded to a
separate quarantine ledger (handle on `TidalDb`), never the community
aggregate, so it is inspectable but does not influence ranking. The default
disposition is `Reject` (loud).
## Done When
A developer can declare two versions of a `GovernancePolicy` for a community in
the schema (v2 with a later `effective_at_ns` than v1), open the database,
write a community-scoped signal whose intent is allowed (it lands in the
community aggregate with its weight clamped into bounds) and one whose intent is
excluded (it is rejected or quarantined and never reaches the aggregate), write
the same excluded intent at `Local` scope and confirm the local ledger records
it untouched, then run a community-scoped RETRIEVE and read back
`Results.policy_metadata.policy_version` / `QueryStats.policy_version` equal to
the version active at query time -- all while every existing M0-M9 test passes
unchanged.