Skip to content

Fusion Governance Lifecycle & Operational Execution

Status & scope

  • Stage: DRAFT — Architecture Decision
  • Milestone: M4 (Platform Integration)

Note: The consent model and FusionRun audit trail are now fully specified in: - component.parallax.three-phase-protocol: Three-Phase Federation Protocol §6 Consent Model, §7 FusionRun Audit Trail - component.parallax.derived-features: Derived Features & Privacy Boundary — privacy guarantees for derived data

This spec covers the broader governance lifecycle (CREATE → ATTEST) which spans cortex/beacon/titan layers. The parallax-level consent and audit implementations are in component.parallax.three-phase-protocol.

Ownership (two-engine model — product.lens / SPEC-LENS + spec-seam-ledger §F1, architect-confirmed 2026-06-03). parallax is the entity-lens engine and owns the entity-lens governance implementation end-to-end: the LensStatus / LensReview / Participation / LensBinding ES persistence and the transition state machines (LENS_TRANSITIONS / PARTICIPATION_TRANSITIONS / BINDING_TRANSITIONS, separation-of-duties, frozen-after-approval semver bump, consent-scope validation, model-first auto-approve). This subsystem is ported into parallax from prism (prism/lens/governance/ + prism/lens/userspace/fusion.py) — prism built it as entity-lens over-reach and retires it under slug 2026-06-03-lens-framework-ownership. Cortex MCP tools and the Beacon Fusion Manager are the management / UI surface over these parallax-owned objects, not a separate governance owner. prism keeps only its own cost-lens governance (full | lightweight | none); the two governance models stay per-engine (dedup via axonis-core only, if ever — parallax SPEC-27).

Problem

parallax solves how to run fusion — the compute layer is complete with 944 passing tests and F1=0.973 on VRS POC (0.967 on Febrl4). component.parallax.fusion-binding defines how data flows into the fusion engine. component.parallax.correlation-persistence defines where results persist.

What's missing is everything before and around execution:

  1. Who creates a lens? A lens defines what data gets compared across organizational boundaries. You can't let anyone create one and run it unchecked.

  2. Who approves it? A lens touching PII across federates needs governance review — are suppressed_fields correct? Is the policy_envelope right? Is the scope appropriate?

  3. What federation does it run in? Which nodes are invited to participate?

  4. Which nodes consent? Each federate must explicitly opt in. "I consent to share soundex(name) + year(dob) for blocking."

  5. How does it run operationally? Not just ad-hoc — streaming on new data, scheduled batch sweeps, reactive to signals from other domains.

  6. What happens with results? Correlations aren't just data points. They're intelligence that drives signals, investigations, and decisions.

The compute is done. The governance and operational layer is the product gap.

Decision

The lens lifecycle has 8 stages, and parallax — the entity-lens engine — owns the implementation of all of them. The LensStatus / LensReview / Participation / LensBinding objects, their ES persistence, and the transition logic that guards each stage live in parallax. ES objects, Cortex MCP tools, and the Beacon Fusion Manager page (driven by experience packs) are the management / UI surface over these parallax-owned objects — they expose and orchestrate the governance, they do not own it.

CREATE → REVIEW → APPROVE → PUBLISH → CONSENT → BIND → EXECUTE → ATTEST
  │         │        │          │          │        │        │         │
  │         │        │          │          │        │     parallax    Invariant 6
  │         │        │          │          │     component.parallax.fusion-binding
  │         │        │          │       Consent model
  │         │        │       Federation topology (UDS)
  │         │     Edition pattern (Invariant 6)
  │      Governance review
  Lens authoring (form or YAML editor)

All eight stages' governance objects — and the transitions between them — are owned and persisted by parallax; the per-stage annotations above name each stage's conceptual driver, not a separate owner. There is no long-running workflow engine and no auto-routing: each transition is an explicit, audited, append-only operation. But it is guarded by a state machine in parallax (LENS_TRANSITIONS / PARTICIPATION_TRANSITIONS / BINDING_TRANSITIONS) that validates the move and enforces separation-of-duties before the new record is written. Cortex MCP tools expose each transition as an independent tool and are the manager; the lens is the configuration; the experience pack decides what shows on the page. The transition logic and ES persistence those tools call live in parallax (ported from prism's lens/governance/ + lens/userspace/fusion.py, slug 2026-06-03-lens-framework-ownership).

The 8 Governance Objects

Object 1: Lens

The lens definition itself. Extends the existing LensSpec with lifecycle metadata.

Index: fusion_lenses
Lifecycle: draft → submitted → approved → active → retired
Field Type Description
lens_id string Unique identifier (e.g., vrs_vulnerability_v1)
version string Semantic version (immutable once any result references it)
status enum draft, submitted, approved, active, retired
spec object The full LensSpec (component.parallax.lens-parser schema)
created_by string Author identity
created_at datetime Creation timestamp
submitted_at datetime When submitted for review (null if draft)
approved_at datetime When governance approved (null if not yet)
activated_at datetime When first execution occurred
retired_at datetime When retired (null if active)
retirement_reason string Why retired (null if active)

Immutability rule: Once a lens reaches approved, the spec field is frozen. Any change to the match function, weights, thresholds, or policy envelope requires a new version. Draft lenses can be edited freely.

Retirement: A retired lens cannot be executed. Existing correlation records produced by it remain valid — they reference the frozen version. Retirement is append-only: a new event, not an update to the original.

Object 2: LensReview

Governance review of a submitted lens. Follows the Edition pattern (Invariant 6: AI assists, humans attest).

Index: fusion_lens_reviews
Lifecycle: pending → approved | rejected | changes_requested
Field Type Description
review_id string Unique review identifier
lens_id string Which lens is being reviewed
lens_version string Which version
reviewer string Governance officer identity
status enum pending, approved, rejected, changes_requested
review_notes string Reviewer's assessment
checklist object Structured review (see below)
created_at datetime When review was assigned
completed_at datetime When decision was made

Review checklist:

checklist:
  scope_appropriate: true        # Is the scope (object types, time window, federation) right?
  suppression_verified: true     # Are suppressed_fields correctly configured?
  policy_envelope_valid: true    # Access level, audit level, classification correct?
  thresholds_justified: true     # Are initial/confirmation thresholds appropriate?
  metrics_appropriate: true      # Do the metrics match the data types?
  weights_balanced: true         # Are field weights reasonable for the domain?
  evidence_rules_sound: true     # Min sources, max age, provenance requirements?
  output_semantics_safe: true    # Signal severity, correlation type, display fields appropriate?

Why not reuse Edition directly? Editions are for decision outcomes in investigations. A lens review is a governance decision on a configuration artifact, not an investigation finding. Different object, same pattern (human reviews, decides, decision is frozen).

Object 3: Federation

Federations already exist in UDS. No new object needed — this spec references them as-is. The Fusion Manager page queries UDS for federation topology.

Existing UDS Data Fusion Manager Use
Federation members Which nodes can be invited to a lens
Federation health Which nodes are online/offline
Federation ABAC What data each node can see

Object 4: Participation

A node's consent to participate in a specific lens within a specific federation. This is the consent model from parallax/ops/fusion/models/consent.py persisted to ES.

Index: fusion_participation
Lifecycle: invited → consented → active → suspended → revoked
Field Type Description
participation_id string Unique identifier
lens_id string Which lens
lens_version string Which version
federation_id string Which federation
federate_id string Which node
status enum invited, consented, active, suspended, revoked
consent_scope object What the node consented to share (see below)
invited_by string Who sent the invitation
invited_at datetime When invited
consented_at datetime When node administrator consented
activated_at datetime When binding was validated and participation became active
suspended_at datetime When temporarily paused (null if active)
revoked_at datetime When permanently withdrawn (null if active)
revocation_reason string Why revoked

Consent scope:

consent_scope:
  blocking_fields:               # What the node shares for blocking
    - field: full_name
      transform: soundex         # "I share soundex of name, not the name itself"
    - field: dob
      transform: year_only       # "I share birth year, not full DOB"
  scoring_fields:                # What the node shares for scoring (feature vectors)
    - field: full_name
      metric: jaro_winkler       # "I allow jaro_winkler comparison of names"
    - field: dob
      metric: exact              # "I allow exact comparison of DOB"
    - field: postcode
      metric: geo_prefix         # "I allow prefix comparison of postcode"
  suppressed_fields:             # What the node will NOT share under any circumstances
    - vulnerability_status
    - case_notes

Consent is granular, not binary. A node doesn't just say "yes, I participate." It says "I consent to share these specific derived values for these specific purposes." This maps directly to ConsentPolicy in parallax's consent model.

Revocation is immediate and append-only. When a node revokes, a new event is created. The node's data is excluded from future fusion runs. Existing correlation records that reference the node's data remain valid (they reference a specific fusion run at a specific point in time) but are flagged as stale (one contributing source revoked).

Object 5: Binding

The per-node schema mapping. Already specified in component.parallax.fusion-binding as LensBinding. The governance lifecycle adds review status tracking.

Index: fusion_bindings (from component.parallax.fusion-binding)
Lifecycle: draft → validated → review → approved → active → invalidated

component.parallax.fusion-binding fully defines the binding format. This spec adds only the governance wrapper:

Additional Field Type Description
participation_id string Links to the node's consent record
validation_result object Schema validation output (field coverage, type mismatches)
review_status enum From component.parallax.fusion-binding: pending, approved, rejected, auto_approved
reviewed_by string Who approved the binding

Auto-approval: When the federate's local_model matches the lens target_model AND all field confidences ≥ 0.90, the binding is auto-approved. This is the model-first resolution from component.parallax.fusion-binding — covers ~80% of nodes at scale.

Object 6: LensActivation

This is the new object. It bridges "lens exists" and "fusion runs." Defines when and how a lens executes, and what happens with results.

Index: fusion_activations
Lifecycle: configured → active → paused → retired
Field Type Description
activation_id string Unique identifier
lens_id string Which lens
lens_version string Which version
federation_id string Which federation
mode enum streaming, adhoc, scheduled, reactive
trigger object What fires execution (see modes below)
execution_config object Scope, threshold overrides, blocking strategy
on_match object Signal generation rules
status enum configured, active, paused, retired
created_by string Who configured the activation
created_at datetime When configured
last_run_at datetime When last executed

Operational counters (run_count, match_count) are not stored on the activation object. They are computed as aggregations over the fusion_runs index filtered by activation_id. This avoids mutable counters on what is otherwise an append-only model. The Fusion Manager UI queries these aggregations for display.

Object 7: FusionRun

Execution instance. One run per activation trigger.

Index: fusion_runs
Lifecycle: queued → running → complete | failed
Field Type Description
run_id string Unique run identifier
activation_id string Which activation triggered this
lens_id string Lens used
lens_version string Lens version
status enum queued, running, complete, failed
started_at datetime Execution start
completed_at datetime Execution end
input_record_count integer Total records across all federates
candidate_pair_count integer Pairs generated by blocking
match_count integer Pairs above threshold
cluster_count integer Entity clusters formed
false_positive_rate float nullable — only meaningful when ground truth is available (primarily dev/test)
timing object Per-phase timing (blocking, scoring, clustering)
error string Error message if failed
participating_federates list Which nodes contributed data

Object 8: CorrelationRecord

Already specified in component.parallax.correlation-persistence and implemented in parallax/ops/fusion/models/correlation.py. The governance layer adds signal generation.

component.parallax.correlation-persistence fully defines the correlation persistence model. This spec adds:

Additional Field Type Description
signal_id string If this correlation generated a signal (null if no signal)
signal_severity enum From lens output_semantics.signal_severity
attested_by string Human who confirmed/rejected (null if pending)

The Four Execution Modes

All four modes use the same parallax compute underneath. The difference is orchestration — what triggers the run, what scope of data, and what happens with results.

Mode 1: Streaming (Real-Time Matching)

Trigger: Data ingest event — a new record arrives in an index watched by an active lens.

Use case: Match every new record as it arrives. "New patient registered — instantly check against all federates."

activation:
  lens_id: vrs_vulnerability_v1
  mode: streaming
  trigger:
    type: ingest
    index_pattern: social_care_*
    filter:
      type: person
  execution_config:
    scope: new_record_vs_all         # match new record against existing blocking keys
    blocking: incremental            # compute blocking key for 1 record, query ES for matches
    threshold: 0.70
  on_match:
    signal_type: fusion_vulnerability_match
    severity: high
    route_to: safeguarding_team

Execution flow:

1. New record arrives in social_care_* index
2. ES watcher / titan message fires trigger
3. Extract features from the new record (parallax)
4. Compute blocking keys for the new record (parallax)
5. Query ES for existing records with matching blocking keys (component.parallax.correlation-persistence acceleration)
6. Score candidate pairs (parallax)
7. If match found → create CorrelationRecord + emit signal
8. Signal appears on analyst's Monitor page

Performance: Sub-second for a single record against millions. You don't re-block the whole dataset — you compute the new record's blocking key, query ES for that key, and score only those candidates.

What parallax needs: score_one_vs_many() — score a single record against a set of candidates. Currently run_fusion() does batch. This is the incremental variant.

Mode 2: Ad-Hoc (Investigation)

Trigger: Analyst action — "Find this person across all participating nodes."

Use case: Investigation query. An analyst has a person of interest and wants to check if they appear in other federates.

activation:
  lens_id: vrs_investigation_v1
  mode: adhoc
  trigger:
    type: manual
    input: single_record             # analyst provides the search entity
  execution_config:
    scope: query_vs_all              # search entity against all federates
    blocking: compute_for_query      # blocking keys from the search entity only
    threshold: 0.60                  # lower threshold for investigation (surface more candidates)
  on_match:
    signal_type: fusion_investigation_hit
    severity: medium
    present_to: requesting_analyst   # results go back to the analyst who searched

Governance question: Does every ad-hoc search need individual approval? No — the governance is on the lens, not each individual search. A pre-approved investigation lens (e.g., vrs_investigation_v1) has already been through the REVIEW → APPROVE cycle. The analyst is authorized to use it by the accountability pack. Each search is logged as a FusionRun with full audit trail.

Mode 3: Scheduled (Batch Pattern Detection)

Trigger: Cron schedule — periodic sweep for patterns.

Use case: "Every Monday, find all new duplicate benefits claims across councils."

activation:
  lens_id: benefits_fraud_detection_v2
  mode: scheduled
  trigger:
    type: cron
    schedule: "0 2 * * MON"           # Every Monday at 2am
  execution_config:
    scope: incremental_since_last     # only records modified since last run
    blocking: incremental             # only recompute blocks for changed records
    threshold: 0.65
    lookback: 7d                      # records modified in last 7 days
  on_match:
    signal_type: fusion_fraud_pattern
    severity: medium
    aggregate: true                   # group related matches into one signal
    route_to: fraud_investigation_team

Execution flow:

1. Cron trigger fires (titan scheduler or ES watcher)
2. Query each federate for records modified since last_run_at
3. Compute blocking keys for changed records (parallax)
4. Cross-reference against existing blocking index (component.parallax.correlation-persistence)
5. Score all new candidate pairs (parallax)
6. Cluster matches (parallax)
7. Create CorrelationRecords for new matches
8. Generate aggregated signal: "47 new potential duplicates found this week"
9. Signal routes to fraud_investigation_team Monitor page

Incremental vs full sweep: Most scheduled runs should be incremental (only process changes since last run). Full sweep is available for initial backfill or periodic re-baseline (e.g., monthly full recalculation).

Mode 4: Reactive (Signal-Triggered Fusion)

Trigger: A signal from one domain triggers fusion in another.

Use case: AML system flags suspicious transaction → fusion runs the entity against PEP lists → if match, critical signal.

activation:
  lens_id: pep_screening_v1
  mode: reactive
  trigger:
    type: signal
    signal_type: aml_suspicious_transaction
    extract_entity: signal.entity_ref  # take this entity from the triggering signal
  execution_config:
    scope: entity_vs_dataset           # match extracted entity against PEP dataset
    dataset: politically_exposed_persons
    threshold: 0.80
  on_match:
    signal_type: pep_match
    severity: critical
    escalate: true                     # immediate escalation

Signal chains: Fusion produces signals. Signals can trigger more fusion. This creates intelligence processing chains — but NOT cycles. Each activation has a defined signal_type trigger and a defined output signal_type. The platform prevents cycles at configuration time (a lens cannot be triggered by the signal type it produces).

The Signal Loop

This is the key architectural insight: fusion produces signals. A correlation IS a signal — "We found the same entity appearing in two federates." The signal feeds back into the platform's existing Monitor → Investigate → Decide workflow.

Data ingested into ES
        ↓
Activation trigger fires (ingest / cron / signal / manual)
        ↓
"Records match an active lens scope"
        ↓
Run fusion (parallax compute)
        ↓
Correlations found?
    ├── Yes → Create CorrelationRecord (component.parallax.correlation-persistence)
    │         ↓
    │   Generate signal (from activation.on_match)
    │         ↓
    │   Signal appears on Monitor page
    │         ↓
    │   Analyst investigates → attests correlation → Edition
    │
    └── No match → log FusionRun with match_count=0, move on

on_match configuration:

Field Type Description
signal_type string Type of signal to generate (maps to experience pack)
severity enum low, medium, high, critical
route_to string Team or role that receives the signal
aggregate boolean Group related matches into one signal (for batch modes)
escalate boolean Immediate escalation (bypasses normal triage)
present_to string Specific user (for ad-hoc mode)
decision_template string Which accountability pack template for attestation

The Fusion Manager Page

A Beacon page driven by experience pack configuration. No intelligence in Beacon — it renders what Cortex returns (ADL-002).

Experience Pack Configuration

fusion_manager:
  page_type: fusion
  title: Fusion Manager
  tabs:
    - id: lenses
      label: Lenses
      view: lens_table
      columns: [lens_id, version, status, domain, created_by, created_at]
      actions: [create, edit, submit_for_review, retire]
      filters: [status, domain]

    - id: reviews
      label: Reviews
      view: review_queue
      columns: [lens_id, version, reviewer, status, submitted_at]
      actions: [approve, reject, request_changes]
      filters: [status, reviewer]

    - id: activations
      label: Activations
      view: activation_table
      columns: [lens_id, mode, status, last_run_at]
      computed_columns: [run_count, match_count]  # aggregated from fusion_runs index
      actions: [configure, activate, pause, retire]
      filters: [mode, status, lens_id]

    - id: participation
      label: Participation
      view: participation_matrix
      rows: federates
      columns: active_lenses
      cell: consent_status
      actions: [invite, view_consent]
      filters: [federation, status]

    - id: runs
      label: Runs
      view: run_history
      columns: [run_id, lens_id, mode, status, started_at, input_records, matches, duration]
      actions: [view_details, rerun]
      filters: [lens_id, status, date_range]

    - id: correlations
      label: Correlations
      view: attestation_queue
      columns: [correlation_id, lens_id, confidence, accuracy, status, contributing_records]
      actions: [attest_confirm, attest_reject, attest_defer, investigate]
      filters: [status, lens_id, confidence_range]

    - id: analytics
      label: Analytics
      view: fusion_dashboard
      widgets:
        - type: kpi
          metrics: [total_correlations, pending_attestation, f1_score, avg_confidence]
        - type: timeseries
          metric: matches_over_time
          group_by: lens_id
        - type: bar
          metric: false_positive_rate
          group_by: lens_id

Tab Descriptions

Lenses — All lens definitions with their lifecycle status. Author, submit for review, track approval, retire. The "Create" action opens a form-based lens editor (simple) or YAML editor (advanced).

Reviews — Governance queue. Lenses awaiting review with the structured checklist. Only users with the governance role (from accountability pack) can approve.

Activations — Operational control plane. How each lens runs. Configure streaming triggers, cron schedules, reactive signal chains. Activate, pause, monitor health.

Participation — Consent matrix. Rows = federates, columns = active lenses. Each cell shows consent status (invited/consented/active/revoked). Drill into consent scope details.

Runs — Execution history. Every fusion run with timing, counts, participating nodes. Filter by lens, status, date range. Click through to see per-pair results.

Correlations — Attestation queue. Match results awaiting human decision. Confidence score, STANAG accuracy/credibility, contributing records. Attest (confirm, reject, defer) or open investigation.

Analytics — Operational metrics. Total correlations over time, pending attestation backlog, F1 score (when ground truth available), false positive rate, processing latency. Per-lens breakdown.

MCP Tools

Cortex tools that expose the governance lifecycle as the management layer — no standalone "fusion manager service." The transition logic and ES persistence they invoke are owned by parallax (the entity-lens engine: the LensStatus / LensReview / Participation / LensBinding state machines + persistence ported from prism); these tools are the interface over parallax-owned objects, not the governance owner.

Lens Management

Tool Action Input Output
create_lens Create draft lens LensSpec YAML or form data lens_id, version
update_lens Edit draft lens lens_id, updated fields updated LensSpec
submit_lens_for_review Submit draft → submitted lens_id, note updated lens (submitted)
review_lens Approve/reject lens lens_id, decision, note, checklist updated status
activate_lens Submitted → active (after approval) lens_id confirmation
retire_lens Active → retired lens_id, reason confirmation
revise_lens Revise approved/active lens → new draft version (§Object 1: any change is a new version; parent-linked) lens_id new lens_id, version
list_lenses Query lenses filters (status, domain, author) Lens[]
get_lens Get single lens with full history lens_id Lens + reviews + runs

Participation & Binding

Tool Action Input Output
invite_node Invite federate to lens lens_id, federate_id, federation_id participation_id
record_consent Node consents with scope participation_id, consent_scope updated status
revoke_participation Node withdraws participation_id, reason updated status
create_binding Create schema binding participation_id, binding config binding_id
validate_binding Validate field coverage (auto-approves on §Object 5 joint condition) binding_id validation_result
submit_binding_for_review Validated → review (non-auto-approval path) binding_id updated status
approve_binding Approve binding under review (review → approved) binding_id updated status
reject_binding Reject binding under review (review → draft) binding_id, reason updated status
list_participation Query participation filters (lens, federation, status) Participation[]
get_participation Get single participation record participation_id Participation
list_bindings Query bindings (Fusion Manager Bindings tab) filters (lens, federate, status) Binding[]
get_binding Get single binding record binding_id Binding

Execution

Tool Action Input Output
configure_activation Set up execution mode lens_id, mode, trigger, on_match activation_id
activate Start activation activation_id confirmation
pause_activation Pause activation_id confirmation
retire_activation Retire (terminal) activation_id, reason confirmation
execute_fusion Trigger manual/ad-hoc run activation_id (or lens_id + query) run_id
get_run_status Check run progress run_id FusionRun status
list_runs Query run history filters (lens, status, date) FusionRun[]

Correlation & Attestation

Tool Action Input Output
list_correlations Query correlations filters (lens, status, confidence) CorrelationRecord[]
get_correlation Get single CR with lineage correlation_id CorrelationRecord + lineage
attest_correlation Human decision correlation_id, decision (confirm/reject/defer) updated status
decay_correlations Apply time decay lens_id, decay_config count of decayed CRs

What parallax Needs

Most of the compute is done. What's needed for operational modes:

Capability Status What's Needed
Batch fusion Done run_fusion(), run_multi_fusion() — existing
Score one vs many Not done score_one_vs_many(query_record, candidates, lens) for streaming/ad-hoc
Incremental blocking Not done Compute blocking key for 1 record, match against existing keys
Lookback window Not done Filter input records by modification date
Activation config model Not done LensActivation dataclass for execution mode + trigger + on_match
Signal generation Not in parallax Cortex concern — parallax returns correlations, Cortex emits signals
Decay computation Done apply_decay() in correlation model — already implemented
Consent enforcement Done check_record_consent(), filter_by_consent() — already implemented
Passport TTL Done check_passport_expiry(), apply_passport_expiry() — already implemented

Heavy compute doesn't change. Metrics, scoring, clustering work as-is. It's the input adapter (one record vs batch) and orchestration wrapper (trigger → scope → compute → signal) that need the four execution modes.

New parallax Interfaces

def score_one_vs_many(
    query_record: dict,
    candidates: list[dict],
    lens: LensSpec,
    binding: LensBinding | None = None,
    consent_policies: list[ConsentPolicy] | None = None,
) -> list[ScoredPair]:
    """Score a single query record against a list of candidates.

    Used by streaming and ad-hoc modes. Computes blocking keys for
    the query record, filters candidates by blocking key match,
    then scores remaining pairs.

    If consent_policies are provided, candidates are filtered through
    filter_by_consent() before scoring — same enforcement as batch
    mode's run_fusion(). Consent enforcement is the caller's
    responsibility in batch mode but must be explicit here because
    streaming callers may not have the full pipeline context.

    Returns scored pairs above lens initial_threshold, sorted by
    confidence descending.
    """

def incremental_blocking_keys(
    record: dict,
    lens: LensSpec,
    binding: LensBinding | None = None,
) -> list[str]:
    """Compute blocking keys for a single record.

    Returns the set of blocking keys that can be used to query
    ES for existing candidate records. Used by streaming mode
    to avoid re-blocking the entire dataset.
    """

CorrelationRecord Decay

The CorrelationRecord lifecycle includes decayed — the first time-dependent state transition in the object model.

Decay is NOT automatic. It is triggered by a scheduled activation or a manual MCP tool call. The decay_correlations tool applies time-based rules from the lens's evidence_rules:

evidence_rules:
  confidence_decay:
    strategy: exponential     # default — matches calculate_decayed_confidence() implementation (0.5^(days/half_life))
    half_life: 90d            # confidence halved after 90 days without re-confirmation
    min_confidence: 0.30      # below this, status → decayed
    reconfirmation_resets: true  # a new fusion run that re-confirms resets the decay clock

Note: strategy: exponential is the only currently implemented strategy in parallax's calculate_decayed_confidence(). Future strategies (linear, step) can be added but are not required for M4.

Why not automatic? Invariant 7 ("no action" is a decision). Decay should be observable and auditable. A scheduled activation (e.g., weekly decay sweep) creates a FusionRun record for the decay operation, with full audit trail of which correlations were affected.

Implementation: parallax already has apply_decay() in the correlation model. The platform schedules the call via a mode: scheduled activation with trigger.type: cron.

Correlation Compaction

At production scale with streaming mode, fusion_correlations can accumulate millions of records — most of them DECAYED or REJECTED. These are dead weight in the hot index: DECAYED CRs don't participate in blocking acceleration (component.parallax.correlation-persistence) and can't be attested. The index needs a compaction strategy.

Compaction is NOT deletion. It moves records from the hot index to a cold archive index. Invariant 2 (append-only) is preserved — the record still exists, just in fusion_correlations_archive. The compaction run itself creates a FusionRun record, maintaining full audit trail.

compaction:
  strategy: archive_after
  decayed_archive_days: 180       # DECAYED CRs older than 180d → archive
  rejected_archive_days: 365      # REJECTED CRs kept longer (negative evidence)
  retain_lineage: true            # archive includes full lineage (audit trail)
  retain_blocking_keys: false     # blocking keys not needed in archive
  trigger: scheduled              # cron activation, same pattern as decay

Why REJECTED CRs persist longer: check_negative_correlation() uses rejected CRs to block known false positives (e.g., two different John Smiths that were manually rejected should not be re-proposed). Archiving too early loses this negative evidence. 365 days gives sufficient coverage for most scheduled re-runs.

Archive index: fusion_correlations_archive has the same mapping as fusion_correlations but uses a cold storage tier. Queries that need historical audit can search both indices. Normal operational queries (attestation queue, blocking acceleration) search only the hot index.

MCP tool:

Tool Action Input Output
compact_correlations Archive old DECAYED/REJECTED CRs lens_id, compaction config count archived, archive index name

Correlation Reconfirmation

When a scheduled or streaming run re-matches a pair that already has a PROPOSED or CONFIRMED CorrelationRecord, the system must handle reconfirmation — not create a duplicate CR.

Reconfirmation flow:

Fusion run scores pair (A, B) at 0.85
    ↓
Check: does a CR already exist for this pair + lens?
    ├── No existing CR → create new CR (normal path)
    └── Existing CR found
        ├── Status: PROPOSED → reconfirm (update confidence, reset decay clock)
        ├── Status: CONFIRMED → reconfirm (update confidence, reset decay clock)
        ├── Status: REJECTED → skip (human already rejected this pair)
        ├── Status: DECAYED → reconfirm (resurrect — re-observation overrides decay)
        └── Status: DEFERRED → reconfirm (new evidence available)

Reconfirmation is a lineage event, not an update. The CR's lineage gains a new RECONFIRMED entry with the new score, new fusion_run_id, and new timestamp. The CR's time_most_recent_observation is updated. The decay clock resets.

What parallax needs:

def reconfirm_correlation(
    cr: CorrelationRecord,
    new_score: float,
    fusion_run_id: str,
    now_iso: str = "",
) -> CorrelationRecord:
    """Reset decay clock and update confidence from re-observation.

    Adds a RECONFIRMED lineage entry. Updates time_most_recent_observation.
    If CR was DECAYED, status reverts to CONFIRMED (resurrection).
    If CR was DEFERRED, status reverts to PROPOSED (new evidence available).
    REJECTED CRs are never reconfirmed — human decision stands.
    """

LineageAction extension: The existing enum in correlation.py has: CREATED, RECORD_ADDED, ATTESTED, INVALIDATED, DECAYED. This spec adds RECONFIRMED — a re-observation event from a subsequent fusion run.

Lens Version Pinning

Activations are pinned to a specific lens version. When a lens goes from v1 → v2:

  1. Existing v1 activations are not auto-migrated. They continue running against v1 until the lens version is retired.
  2. When lens v1 is retired, all activations referencing v1 are automatically paused. An administrator must create new activations for v2.
  3. An administrator may create v2 activations while v1 activations are still active — this allows gradual rollover (run both versions in parallel, compare results, retire v1 when confident).
  4. CorrelationRecords always reference the lens_version that produced them. A CR from v1 and a CR from v2 for the same pair are distinct records with distinct lineage.

Why not auto-migrate? A new lens version may change weights, thresholds, metrics, or field scope. Auto-migrating activations would silently change operational behavior — violating the frozen principle (Invariant 4) and bypassing governance review of the activation's effective configuration.

Streaming Debounce

Streaming mode fires on every ingest event. If 100 records arrive in a batch, 100 independent score_one_vs_many() calls would create duplicate work — records 5 and 47 might match each other, producing two CRs for the same pair from two separate micro-runs. Additionally, 100 simultaneous triggers create a thundering herd problem.

Solution: micro-batch streaming. The trigger includes a debounce window that collects ingest events before dispatching a single fusion run.

trigger:
  type: ingest
  index_pattern: social_care_*
  filter:
    type: person
  debounce: 5s              # batch ingest events within 5s window
  max_batch_size: 100       # process up to 100 new records per micro-batch

Behavior: - Ingest events accumulate for debounce seconds (default 5s). - When the window closes (or max_batch_size is reached), a single fusion run processes all accumulated records together. - Within the micro-batch, records are cross-compared (catching intra-batch matches) AND compared against the existing index (catching cross-batch matches). - This turns "pure streaming" into "micro-batch streaming" — same parallax compute, amortized trigger overhead, no duplicate CRs.

Latency trade-off: 5s debounce means a new record is matched within ~5-10s of ingest (5s accumulation + scoring time). For most use cases this is acceptable. Critical use cases can set debounce: 0s for true per-record streaming at the cost of higher load and potential duplicates (which the reconfirmation flow handles gracefully).

Invariant Compliance

Invariant Compliance
1. UDS is sole ABAC authority Participation queries UDS for federation topology. Consent scope is enforced by parallax's filter_by_consent(). Data access governed by UDS at query time.
2. Events are append-only All 8 objects use append-only state transitions. Revocation creates a new event. Decay creates a new event. No UPDATE, no DELETE.
3. Blocks are evidence Every FusionRun creates evidence blocks with query hash, lens version, binding version, timing, and provenance.
4. Frozen means frozen Approved lenses are frozen. Binding spec frozen after approval. CorrelationRecords are frozen at creation.
5. Editions require frozen evidence Attestation (confirm/reject) on a correlation references a frozen CorrelationRecord backed by a frozen FusionRun.
6. AI assists, humans attest AI computes matches. Governance officers approve lenses. Node administrators consent. Analysts attest correlations. Every human decision is a distinct object.
7. "No action" is a decision Unattested correlations remain proposed — they don't auto-confirm. deferred is an explicit attestation decision. Decay is a scheduled decision, not a silent expiry.

Relationship to Existing Specs

Spec Relationship
component.parallax.lens-parser (Lens Parser) Lens authoring uses parse_lens() + validate_lens(). LensSpec is the spec field of the Lens governance object.
component.parallax.fusion-binding (Binding & Project) Binding format, project DAG, FUSION_ENGINE node — all used as-is. This spec adds governance lifecycle wrapper.
component.parallax.correlation-persistence (Correlation Persistence) CorrelationRecord persistence, blocking acceleration — used by all four execution modes. Streaming mode relies heavily on component.parallax.correlation-persistence's incremental blocking index.
component.parallax.primitives-framework (Primitives) All 94 primitives run in parallax compute. This spec doesn't change what computes — it governs when and how compute is triggered.

What Gets Built (In Order)

Phase A: Governance Foundation (Platform)

  1. ES mappings for fusion_lenses, fusion_lens_reviews, fusion_participation, fusion_activations, fusion_runs
  2. Lens management MCP tools: create_lens, submit_lens_for_review, review_lens, activate_lens, retire_lens, list_lenses
  3. Participation MCP tools: invite_node, record_consent, revoke_participation
  4. Experience pack config for Fusion Manager page (Lenses + Reviews + Participation tabs)

Phase B: Operational Execution (parallax + Platform)

  1. LensActivation dataclass in parallax
  2. score_one_vs_many() and incremental_blocking_keys() in parallax
  3. Activation MCP tools: configure_activation, activate, pause_activation
  4. Execution MCP tools: execute_fusion, get_run_status, list_runs
  5. Integration with titan for trigger dispatch (ingest watcher, cron scheduler, signal listener)
  6. Experience pack config for Activations + Runs tabs

Phase C: Intelligence Output (Platform)

  1. Signal generation from correlations (Cortex → Monitor page integration)
  2. Correlation attestation MCP tools: list_correlations, attest_correlation
  3. Experience pack config for Correlations + Analytics tabs
  4. Reactive mode: signal-triggered lens execution
  5. Cycle detection for reactive signal chains

Deferred

  • Lens authoring UI (Drawflow node builder for visual lens design)
  • LLM-assisted lens creation ("describe what you want to match, I'll generate the lens")
  • Multi-lens orchestration (run lens A's output as input to lens B)
  • Cross-federation lens sharing (lens templates published to a lens marketplace)

Cross-References

Document Relationship
component.parallax.lens-parser (Lens Parser) Lens authoring input contract
component.parallax.fusion-binding (Binding & Project) Binding format and project DAG
component.parallax.correlation-persistence (Correlation Persistence) Output persistence and blocking acceleration
component.parallax.scoring-engine (Scoring Engine) Scoring thresholds govern attestation requirements
models/correlation.py CorrelationRecord, Attestation, decay — already implemented
models/consent.py ConsentPolicy, consent checking — already implemented
models/binding.py LensBinding, ReviewStatus — already implemented
models/project.py FusionProject, ProjectStatus — already implemented
docs/ARCHITECTURE.md Module structure, clean architecture layering

Depends on: component.parallax.correlation-persistence, component.parallax.fusion-binding, component.parallax.lens-parser

Realizes: product.fusion, product.lens

Required by: component.parallax.wire-message-families