tidaldb/tidal/docs/planning/milestone-10/phase-1/OVERVIEW.md
jx12n 3bcfb3c576 feat: Bazel build, crate docs/ai-lookup, docker images, and engine hardening
- Add BUILD.bazel across tidal, tidal-net, tidal-server, tidalctl for bzlmod build
- Add tidal/ crate docs (README, CHANGELOG, CONTRIBUTING, AGENTS, CLAUDE, API, ARCHITECTURE) and ai-lookup reference
- Add docker standalone/cluster/deploy images, compose, and prometheus config
- Harden WAL (batch format, writer, dedup, diagnostics), text syncer/collectors, and vector registry
- Expand tidalctl CLI and tests; restructure WAL/visibility integration test suites
- Refine tidal-net transport/client/server and tidal-server cluster/scatter-gather
2026-06-07 18:29:38 -06:00

27 KiB

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

// 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

// 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.

// 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)

// 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

// 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.

// 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
}
// 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

// 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:

// 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.

// 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.