///|
/// Scope is part of identity even if item and predicate edges share endpoints.
pub struct RelationScopedEdgeV4 {
  priv dependency : Edge
  priv scope : String
} derive(Eq, Hash)

///|
pub extend RelationScopedEdgeV4 with Eq::{equal, not_equal}

///|
pub extend RelationScopedEdgeV4 with Hash::{hash, hash_combine}

///|
pub fn RelationScopedEdgeV4::scope(self : RelationScopedEdgeV4) -> String {
  self.scope
}

///|
pub fn RelationScopedEdgeV4::dependency(self : RelationScopedEdgeV4) -> Edge {
  self.dependency
}

///|
priv struct RelationPredicateBudgetV4 {
  max_evaluations : Int32
  max_candidates : Int32
  mut evaluations : Int32
  mut candidates : Int32
  mut limited : Bool
  mut reason : String
}

///|
priv struct RelationPredicateQueryV4 {
  tx : String
  table : Int32
  predicate : Array[RelationV3Predicate]
  rows : Array[RelationV3Row]
  seq : Int64
  query_index : Int32
  own : Map[Int32, Int32]
  mut complete : Bool
  domains : Array[Json]
}

///|
/// Persistent primary-key slots are the objects of this explicit adapter.
/// Original tuple incarnations are retained; their physical equivalence is not claimed.
pub struct RelationPredicateGraphV4 {
  priv index : RelationPointIndexV4
  priv input_contract : Json
  priv queries : Array[RelationPredicateQueryV4]
  priv scoped_edges : Array[RelationScopedEdgeV4]
  priv sources : Map[RelationScopedEdgeV4, Array[Json]]
  priv diagnostics : Array[Json]
  priv gaps : Array[Json]
  priv graph_checks : Int32
  priv graph_limit : Int32
  priv graph_limited : Bool
  priv predicate_budget : RelationPredicateBudgetV4
  priv complete : Bool
}

///|
pub fn RelationPredicateGraphV4::edges(
  self : RelationPredicateGraphV4,
) -> Array[RelationScopedEdgeV4] {
  self.scoped_edges.copy()
}

///|
fn relation_predicate_charge_v4(
  budget : RelationPredicateBudgetV4,
  candidate~ : Bool = false,
) -> Bool {
  if budget.limited {
    return false
  }
  if candidate {
    if budget.candidates >= budget.max_candidates {
      budget.limited = true
      budget.reason = "predicate-candidate-visit-budget"
      return false
    }
    budget.candidates += 1
  } else {
    if budget.evaluations >= budget.max_evaluations {
      budget.limited = true
      budget.reason = "predicate-evaluation-budget"
      return false
    }
    budget.evaluations += 1
  }
  true
}

///|
fn relation_predicate_gap_v4(
  gaps : Array[Json],
  code : String,
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
  key : Int32,
) -> Unit {
  query.complete = false
  gaps.push({
    "code": Json(code),
    "transaction": Json(query.tx),
    "readSeq": Json(query.seq.to_double()),
    "queryIndex": Json(query.query_index),
    "identity": relation_point_key_json_v4(index, key),
    "original": index.originals.get_or_default(query.seq, Json::null()),
  })
}

///|
fn relation_predicate_query_json_v4(
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
) -> Json {
  {
    "transaction": Json(query.tx),
    "relation": Json(index.parsed.tables[query.table].name),
    "readSeq": Json(query.seq.to_double()),
    "queryIndex": Json(query.query_index),
    "original": index.originals.get_or_default(query.seq, Json::null()),
  }
}

///|
fn relation_predicate_matches_v4(
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
  source : Int32,
  budget : RelationPredicateBudgetV4,
) -> Bool? {
  if !relation_predicate_charge_v4(budget) {
    return None
  }
  if source < 0 || index.parsed.sources[source].action == "delete" {
    return Some(false)
  }
  let row = index.parsed.sources[source].row
  for predicate in query.predicate {
    if !relation_predicate_charge_v4(budget) {
      return None
    }
    if !relation_v3_predicate_matches(predicate, row, index.keys) {
      return Some(false)
    }
  }
  Some(true)
}

///|
fn relation_predicate_queries_v4(
  index : RelationPointIndexV4,
  budget : RelationPointBudgetV4,
) -> Array[RelationPredicateQueryV4] {
  let result : Array[RelationPredicateQueryV4] = []
  for program in index.parsed.programs {
    let own : Map[Int32, Int32] = Map([])
    let trail : Array[(Int32, Int32)] = []
    let scopes : Array[(String, Int32)] = []
    let mut query_index = 0
    for action in program.actions {
      if !relation_point_charge_v4(budget) {
        return result
      }
      match action {
        WriteRow(source) => {
          let key = index.parsed.sources[source].row.key
          trail.push((key, own.get_or_default(key, -2)))
          own[key] = source
        }
        QueryRows(table, predicate, rows, seq) => {
          result.push({
            tx: program.id,
            table,
            predicate,
            rows,
            seq,
            query_index,
            own: own.copy(),
            complete: false,
            domains: [],
          })
          query_index += 1
        }
        Mark(instance) => scopes.push((instance, trail.length()))
        Undo(instance) | Release(instance) => {
          let mut found = -1
          for position, scope in scopes {
            if scope.0 == instance {
              found = position
            }
          }
          if found < 0 {
            return result
          }
          match action {
            Undo(_) =>
              while trail.length() > scopes[found].1 {
                let (key, previous) = trail.pop().unwrap()
                if previous == -2 {
                  let _ = own.remove(key)
                } else {
                  own[key] = previous
                }
              }
            _ => ()
          }
          let retained = if action is Undo(_) { found + 1 } else { found }
          while scopes.length() > retained {
            let _ = scopes.pop()
          }
        }
      }
    }
  }
  result
}

///|
fn relation_predicate_emit_v4(
  edges : Array[RelationScopedEdgeV4],
  sources : Map[RelationScopedEdgeV4, Array[Json]],
  graph_budget : RelationPointBudgetV4,
  dependency : Edge,
  scope : String,
  proof : Json,
) -> Bool {
  if dependency.from == dependency.to {
    return true
  }
  if !relation_point_charge_v4(graph_budget) {
    return false
  }
  let edge : RelationScopedEdgeV4 = { dependency, scope, }
  match sources.get(edge) {
    Some(proofs) => proofs.push(proof)
    None => {
      if edges.length() >= 50000 {
        graph_budget.limited = true
        graph_budget.reason = "relation-predicate-edge-budget-50000"
        return false
      }
      edges.push(edge)
      sources[edge] = [proof]
    }
  }
  true
}

///|
fn relation_predicate_edge_v4(
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
  key : Int32,
  source : Int32,
  selected : Int32,
  kind : Dependency,
  scope : String,
) -> Edge {
  let writer = index.parsed.sources[source].writer.unwrap()
  let version = index.parsed.sources[source].row.version
  let selected_version = if selected >= 0 {
    Some(index.parsed.sources[selected].row.version)
  } else {
    None
  }
  {
    from: if kind == WR {
      writer
    } else {
      query.tx
    },
    to: if kind == WR {
      query.tx
    } else {
      writer
    },
    kind,
    key: Some("relation-point-key-" + key.to_string()),
    from_op: if kind == WR {
      None
    } else {
      Some(query.query_index)
    },
    to_op: if kind == WR {
      Some(query.query_index)
    } else {
      None
    },
    from_version: if kind == WR {
      Some(version)
    } else {
      selected_version
    },
    to_version: if kind == WR {
      selected_version
    } else {
      Some(version)
    },
    reason: if scope == "predicate" {
      if kind == WR {
        "Every remaining selected slot version is at or after this installed membership toggle"
      } else {
        "Every remaining selected slot version precedes this installed membership toggle"
      }
    } else if kind == WR {
      "The actual returned row identifies this external installed-final item version"
    } else {
      "This is the next installed version of the actual external returned item; own successor stops"
    },
  }
}

///|
fn relation_predicate_source_v4(
  index : RelationPointIndexV4,
  source : Int32,
) -> Json {
  if source < 0 {
    {
      "kind": "initial-absent-key-slot",
      "writer": Json::null(),
      "version": Json::null(),
    }
  } else {
    relation_point_source_json_v4(index, source)
  }
}

///|
fn relation_predicate_query_valid_v4(
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
  gaps : Array[Json],
) -> Bool {
  if index.outcomes.get(query.tx) != Some(Committed) {
    query.complete = false
    return false
  }
  let raw = index.originals.get_or_default(query.seq, Json::null())
  if relation_classification_at_v4(raw, ["kind"]) != Json("row_read") ||
    relation_classification_at_v4(raw, ["complete"]) != Json(true) {
    relation_predicate_gap_v4(
      gaps, "predicate-result-not-completely-delivered", index, query, -1,
    )
    return false
  }
  if !relation_point_program_known_v4(index, query.tx, query.seq) {
    relation_predicate_gap_v4(
      gaps, "predicate-unproved-program-prefix", index, query, -1,
    )
    return false
  }
  let seen : Map[Int32, Bool] = Map([])
  for row in query.rows {
    if seen.contains(row.key) || index.keys[row.key].table != query.table {
      relation_predicate_gap_v4(
        gaps,
        "predicate-result-key-or-uniqueness-mismatch",
        index,
        query,
        row.key,
      )
      return false
    }
    seen[row.key] = true
  }
  query.complete = true
  true
}

///|
fn relation_predicate_positive_v4(
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
  key : Int32,
  row : RelationV3Row,
  gaps : Array[Json],
  diagnostics : Array[Json],
  predicate_budget : RelationPredicateBudgetV4,
) -> Int32 {
  let source_index = index.versions.get_or_default(row.version, -1)
  if source_index < 0 {
    relation_predicate_gap_v4(
      gaps, "predicate-returned-row-source-missing", index, query, key,
    )
    return -3
  }
  let source = index.parsed.sources[source_index]
  if source.row.key != key ||
    source.row.values != row.values ||
    source.action == "delete" ||
    source.seq >= query.seq {
    relation_predicate_gap_v4(
      gaps, "predicate-returned-row-source-mismatch", index, query, key,
    )
    return -3
  }
  if relation_predicate_matches_v4(index, query, source_index, predicate_budget) !=
    Some(true) {
    relation_predicate_gap_v4(
      gaps, "returned-row-does-not-prove-predicate-membership", index, query, key,
    )
    return -3
  }
  if query.own.contains(key) {
    if query.own.at(key) != source_index {
      relation_predicate_gap_v4(
        gaps, "predicate-own-delivered-row-state-mismatch", index, query, key,
      )
      return -3
    }
    return source_index
  }
  match source.writer {
    None => source_index
    Some(writer) => {
      let code = if writer == query.tx {
        "predicate-own-returned-row-not-active"
      } else if index.outcomes.get(writer) == Some(Aborted) ||
        !index.surviving[source_index] {
        "G1a"
      } else if index.outcomes.get(writer) != Some(Committed) {
        "predicate-returned-row-writer-outcome-unknown"
      } else if !index.final_set[source_index] {
        "G1b"
      } else {
        ""
      }
      if code != "" {
        relation_predicate_gap_v4(
          gaps, "predicate-returned-row-not-installed-final", index, query, key,
        )
        diagnostics.push({
          "code": Json(code),
          "observation": relation_predicate_query_json_v4(index, query),
          "identity": relation_point_key_json_v4(index, key),
          "actualReturnedSource": relation_point_source_json_v4(
            index, source_index,
          ),
          "basis": "actual returned row identity, not a guessed negative or predicate-write result",
        })
        -3
      } else {
        source_index
      }
    }
  }
}

///|
fn relation_predicate_closed_window_v4(index : RelationPointIndexV4) -> Bool {
  !index.window_unproven &&
  relation_classification_at_v4(index.parsed.normalized, [
    "coverage", "complete",
  ]) ==
  Json(true)
}

///|
fn relation_predicate_domain_v4(
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
  key : Int32,
  source_indices : Array[Int32],
  gaps : Array[Json],
  budget : RelationPredicateBudgetV4,
) -> (Array[Int32], Bool) {
  let candidates : Array[Int32] = []
  let closed = relation_predicate_closed_window_v4(index)
  if !closed {
    relation_predicate_gap_v4(
      gaps, "predicate-negative-window-not-closed", index, query, key,
    )
  }
  if relation_point_initial_v4(index, key) < 0 {
    if !relation_predicate_charge_v4(budget, candidate=true) {
      return (candidates, false)
    }
    candidates.push(-1)
  }
  for source in source_indices {
    if !relation_predicate_charge_v4(budget, candidate=true) {
      return (candidates, false)
    }
    match relation_predicate_matches_v4(index, query, source, budget) {
      Some(false) => candidates.push(source)
      Some(true) => ()
      None => return (candidates, false)
    }
  }
  if candidates.is_empty() {
    relation_predicate_gap_v4(
      gaps, "predicate-negative-has-no-observed-source", index, query, key,
    )
  }
  (candidates, closed)
}

///|
fn relation_predicate_toggles_v4(
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
  key : Int32,
  budget : RelationPredicateBudgetV4,
) -> Array[Int32]? {
  let initial = relation_point_initial_v4(index, key)
  let mut previous = match
    relation_predicate_matches_v4(index, query, initial, budget) {
    Some(value) => value
    None => return None
  }
  let toggles : Array[Int32] = []
  for source in index.chains[key] {
    let current = match
      relation_predicate_matches_v4(index, query, source, budget) {
      Some(value) => value
      None => return None
    }
    if previous != current {
      toggles.push(source)
    }
    previous = current
  }
  Some(toggles)
}

///|
fn relation_predicate_key_edges_v4(
  index : RelationPointIndexV4,
  query : RelationPredicateQueryV4,
  key : Int32,
  candidates : Array[Int32],
  positive : Bool,
  candidate_closed : Bool,
  edges : Array[RelationScopedEdgeV4],
  sources : Map[RelationScopedEdgeV4, Array[Json]],
  gaps : Array[Json],
  graph_budget : RelationPointBudgetV4,
  budget : RelationPredicateBudgetV4,
) -> Unit {
  let selected = if candidates.length() == 1 { candidates[0] } else { -1 }
  // Positive item WR requires no version order, but it does require the exact
  // externally returned installed-final row; own-at-delivery is not external.
  if positive && selected >= 0 && !query.own.contains(key) {
    match index.parsed.sources[selected].writer {
      Some(writer) if writer != query.tx => {
        let _ = relation_predicate_emit_v4(
          edges,
          sources,
          graph_budget,
          relation_predicate_edge_v4(
            index,
            query,
            key,
            selected,
            selected,
            WR,
            "item",
          ),
          "item",
          {
            "basis": "actual-returned-row-version",
            "query": relation_predicate_query_json_v4(index, query),
            "identity": relation_point_key_json_v4(index, key),
            "itemSource": relation_point_source_json_v4(index, selected),
          },
        )
      }
      _ => ()
    }
  }
  if candidates.is_empty() {
    return
  }
  if query.own.contains(key) {
    // The local overlay is a proved observation, including a later-rolled-back
    // write, but its placement in installed predicate version order is separate.
    relation_predicate_gap_v4(
      gaps, "predicate-own-view-needs-installed-order-proof", index, query, key,
    )
    return
  }
  if index.blocked.contains(key) || !index.ordered.contains(key) {
    relation_predicate_gap_v4(
      gaps, "predicate-version-order-not-complete", index, query, key,
    )
    return
  }
  let mut minimum = 2147483647
  let mut maximum = -1
  let mut ranked = true
  for source in candidates {
    let rank = if source < 0 { 0 } else { index.ranks[source] }
    if rank < 0 {
      ranked = false
    } else {
      minimum = if rank < minimum { rank } else { minimum }
      maximum = if rank > maximum { rank } else { maximum }
    }
  }
  if !ranked {
    relation_predicate_gap_v4(
      gaps, "predicate-candidate-source-not-installed", index, query, key,
    )
    return
  }
  if !candidate_closed {
    return
  }
  let toggles = match relation_predicate_toggles_v4(index, query, key, budget) {
    Some(value) => value
    None => return
  }
  // Different unobserved negative versions need not imply different predicate
  // dependencies. A membership toggle between two candidates is the boundary
  // that changes a WR/RW relation. Preserve every candidate; never pick a read.
  let predicate_equivalent = !toggles.any(source => {
    let rank = index.ranks[source]
    rank > minimum && rank <= maximum
  })
  if !predicate_equivalent {
    relation_predicate_gap_v4(
      gaps, "predicate-negative-source-ambiguous", index, query, key,
    )
  }
  let candidate_facts = candidates.map(source => {
    relation_predicate_source_v4(index, source)
  })
  let universal : Json = {
    "basis": "all-remaining-candidates; no selected negative version is asserted as observed",
    "query": relation_predicate_query_json_v4(index, query),
    "identity": relation_point_key_json_v4(index, key),
    "actualReturnedRow": Json(positive),
    "candidateDomainClosed": true,
    "candidatePositionsArePredicateEquivalent": Json(predicate_equivalent),
    "candidateSources": Json(candidate_facts),
    "minimumInstalledRank": Json(minimum),
    "maximumInstalledRank": Json(maximum),
  }
  for source in toggles {
    let rank = index.ranks[source]
    let kind = if rank <= minimum {
      WR
    } else if rank > maximum {
      RW
    } else {
      continue
    }
    // Shared evidence is private and immutable; only the public JSON exit
    // deep-copies it. Do not copy the entire delivered result for every edge.
    let fields = object_fields_unchecked_v3(universal).copy()
    let proof = Json::object(fields)
    fields["rule"] = Json(
      if kind == WR {
        "adya-2000-definition-3-all-prior-or-equal-toggles"
      } else {
        "adya-2000-definitions-4-5-any-later-toggle"
      },
    )
    fields["toggleRank"] = Json(rank)
    fields["installedToggle"] = relation_point_source_json_v4(index, source)
    let previous = if rank == 1 {
      relation_point_initial_v4(index, key)
    } else {
      index.chains[key][rank - 2]
    }
    fields["directPreviousSlotVersion"] = relation_predicate_source_v4(
      index, previous,
    )
    if !relation_predicate_emit_v4(
        edges,
        sources,
        graph_budget,
        relation_predicate_edge_v4(
          index, query, key, source, selected, kind, "predicate",
        ),
        "predicate",
        proof,
      ) {
      return
    }
  }
  if positive && selected >= 0 {
    let rank = index.ranks[selected]
    let chain = index.chains[key]
    if rank >= 0 && rank < chain.length() {
      let next = chain[rank]
      if index.parsed.sources[next].writer != Some(query.tx) {
        let _ = relation_predicate_emit_v4(
          edges,
          sources,
          graph_budget,
          relation_predicate_edge_v4(
            index,
            query,
            key,
            next,
            selected,
            RW,
            "item",
          ),
          "item",
          {
            "basis": "actual-external-returned-item-to-direct-installed-successor",
            "directSuccessor": true,
            "query": relation_predicate_query_json_v4(index, query),
            "identity": relation_point_key_json_v4(index, key),
            "itemSource": relation_point_source_json_v4(index, selected),
            "nextInstalledEffect": relation_point_source_json_v4(index, next),
          },
        )
      }
    }
  }
}

///|
fn relation_predicate_graph_parsed_v4(
  parsed : RelationV3Parsed,
  input_contract : Json,
  window_unproven : Bool,
  max_graph_checks : Int32,
  max_predicate_checks : Int32,
  max_candidate_visits : Int32,
) -> RelationPredicateGraphV4 {
  let graph_budget : RelationPointBudgetV4 = {
    maximum: max_graph_checks,
    checks: 0,
    limited: false,
    reason: "complete",
  }
  let budget : RelationPredicateBudgetV4 = {
    max_evaluations: max_predicate_checks,
    max_candidates: max_candidate_visits,
    evaluations: 0,
    candidates: 0,
    limited: false,
    reason: "complete",
  }
  let index = relation_point_empty_index_v4(parsed)
  index.window_unproven = window_unproven
  let edges : Array[RelationScopedEdgeV4] = []
  let sources : Map[RelationScopedEdgeV4, Array[Json]] = Map([])
  let diagnostics : Array[Json] = []
  let gaps : Array[Json] = []
  let mut queries : Array[RelationPredicateQueryV4] = []
  let valid = max_graph_checks >= 0 &&
    max_graph_checks <= 1000000 &&
    max_predicate_checks >= 0 &&
    max_predicate_checks <= 1000000 &&
    max_candidate_visits >= 0 &&
    max_candidate_visits <= 100000
  let capacity = parsed.programs.length() <= 128 && parsed.operations <= 4096
  if valid && capacity {
    relation_point_journal_v4(index, graph_budget)
    if index.journal_complete && !graph_budget.limited {
      relation_point_orders_v4(index, graph_budget)
    }
    if index.journal_complete && !graph_budget.limited {
      queries = relation_predicate_queries_v4(index, graph_budget)
    }
    // Raw delete/reinsert tuple identities cannot silently become a claim of
    // equivalence with physical entity-incarnation Adya histories.
    let incarnation_keys : Map[Int32, Bool] = Map([])
    let by_key : Map[Int32, Array[Int32]] = Map([])
    for position, source in parsed.sources {
      if !relation_point_charge_v4(graph_budget) {
        break
      }
      let items = by_key.get_or_default(source.row.key, [])
      items.push(position)
      by_key[source.row.key] = items
      if source.action == "insert" {
        match source.predecessor {
          Some(version) =>
            match index.versions.get(version) {
              Some(previous) if parsed.sources[previous].action == "delete" => {
                incarnation_keys[source.row.key] = true
                gaps.push({
                  "code": "physical-incarnation-equivalence-unproven",
                  "identity": relation_point_key_json_v4(index, source.row.key),
                  "original": index.originals.at(source.seq),
                  "adapter": "persistent-primary-key-slot",
                })
              }
              _ => ()
            }
          _ => ()
        }
      }
    }
    // Complete WW paths use final installed effects, never intermediate writes.
    for key, chain in index.chains {
      if index.blocked.contains(key) || incarnation_keys.contains(key) {
        continue
      }
      for rank in 1.. candidates = [selected]
            Some(true) => {
              relation_predicate_gap_v4(
                gaps, "predicate-own-matching-row-not-delivered", index, query, key,
              )
              continue
            }
            None => closed = false
          }
        } else {
          let domain = relation_predicate_domain_v4(
            index,
            query,
            key,
            by_key.get_or_default(key, []),
            gaps,
            budget,
          )
          candidates = domain.0
          closed = domain.1
        }
        query.domains.push({
          "identity": relation_point_key_json_v4(index, key),
          "actualReturnedRow": Json(positive),
          "ownAtDelivery": Json(query.own.contains(key)),
          "closed": Json(closed),
          "candidateSources": Json(
            candidates.map(source => relation_predicate_source_v4(index, source)),
          ),
          "inferredConstraintOnly": Json(!positive && !query.own.contains(key)),
          "wildcardSource": Json(!closed),
        })
        relation_predicate_key_edges_v4(
          index, query, key, candidates, positive, closed, edges, sources, gaps,
          graph_budget, budget,
        )
      }
      if budget.limited || graph_budget.limited {
        query.complete = false
      }
    }
  } else {
    gaps.push({
      "code": Json(
        if valid {
          "relation-predicate-capacity-128-4096"
        } else {
          "invalid-relation-predicate-budget"
        },
      ),
    })
  }
  for tx, boundary in index.uncertain_after {
    gaps.push({
      "code": "predicate-program-prefix-unproven",
      "transaction": Json(tx),
      "firstUnknownSeq": Json(boundary.to_double()),
    })
  }
  for tx, status in index.outcomes {
    if status == Unknown {
      gaps.push({
        "code": "predicate-transaction-outcome-unconfirmed",
        "transaction": Json(tx),
      })
    }
  }
  if index.window_unproven {
    gaps.push({ "code": "predicate-input-window-unproven" })
  }
  for gap in index.gaps {
    gaps.push(gap)
  }
  if budget.limited || graph_budget.limited {
    gaps.push({
      "code": Json(
        if budget.limited {
          budget.reason
        } else {
          graph_budget.reason
        },
      ),
    })
  }
  let complete = valid &&
    capacity &&
    index.journal_complete &&
    index.orders_complete &&
    !graph_budget.limited &&
    !budget.limited &&
    gaps.is_empty() &&
    queries.all(query => {
      query.complete || index.outcomes.get(query.tx) != Some(Committed)
    })
  {
    index,
    input_contract,
    queries,
    scoped_edges: edges,
    sources,
    diagnostics,
    gaps,
    graph_checks: graph_budget.checks,
    graph_limit: max_graph_checks,
    graph_limited: graph_budget.limited,
    predicate_budget: budget,
    complete,
  }
}

///|
pub fn build_relation_predicate_graph_v4(
  history : HistoryV4,
  max_graph_checks~ : Int32 = 1000000,
  max_predicate_checks~ : Int32 = 1000000,
  max_candidate_visits~ : Int32 = 100000,
) -> Result[RelationPredicateGraphV4, String] {
  let metadata = object_fields_unchecked_v3(history.source_document).copy()
  let _ = metadata.remove("events")
  match history.payload {
    RelationSnapshot(parsed) =>
      Ok(
        relation_predicate_graph_parsed_v4(
          parsed,
          Json::object(metadata),
          relation_point_window_unproven_v4(history),
          max_graph_checks,
          max_predicate_checks,
          max_candidate_visits,
        ),
      )
    _ =>
      Err(
        "relation-predicate graph requires relation-set-v1 original capture facts",
      )
  }
}

///|
pub fn relation_predicate_graph_v4_json(
  result : RelationPredicateGraphV4,
) -> Json {
  let index = result.index
  let budget = result.predicate_budget
  let record : Json = {
    "format": "moontxnwitness-relation-predicate-graph/v4",
    "schemaVersion": 4,
    "semanticContract": "adya-2000-predicate-on-keyslots-v1",
    "algorithm": "relation-predicate-must-edges-3",
    "indexAlgorithm": "relation-point-local-effect-journal-4",
    "acceptsIsolationModels": false,
    "dataModel": "relation-set-v1",
    "objectAdapter": {
      "kind": "persistent-primary-key-slot",
      "deleteState": "Absent",
      "insertState": "Present",
      "physicalEntityIncarnationEquivalence": "not-claimed",
      "crossIncarnationUnproven": "explicit-source-gap",
    },
    "complete": Json(result.complete),
    "inputContract": result.input_contract,
    "observations": object_fields_unchecked_v3(index.parsed.normalized).at(
      "observations",
    ),
    "writeEffects": object_fields_unchecked_v3(index.parsed.normalized).at(
      "effects",
    ),
    "inferences": {
      "negativeSelection": "constraint candidates, never a fabricated observed negative version",
      "dependencyStrength": "must edge shared by every closed candidate interpretation",
      "versionOrderCompletion": false,
      "predicateReadRule": "Def3: all installed equal-or-prior local membership toggles",
      "predicateAntiRule": "Def4/5: any installed later local membership toggle",
      "itemAntiRule": "external actual returned item to next installed successor; own successor stops",
      "queries": Json(
        result.queries.map(query => {
          (
            {
              "observation": relation_predicate_query_json_v4(index, query),
              "complete": Json(query.complete),
              "candidateDomains": Json(query.domains),
            } : Json)
        }),
      ),
      "sourceAudit": Json(
        index.parsed.sources.mapi((source, _) => {
          relation_point_source_json_v4(index, source)
        }),
      ),
    },
    "scopedEdges": Json(
      result.scoped_edges.map(edge => {
        (
          {
            "scope": Json(edge.scope),
            "dependency": edge_json(edge.dependency),
            "sources": Json(result.sources.at(edge)),
          } : Json)
      }),
    ),
    "gaps": Json(result.gaps),
    "diagnostics": Json(result.diagnostics),
    "execution": {
      "counterContract": "relation-predicate-source-evaluation-candidates-2",
      "predicateEvaluationUnit": "one membership probe plus each evaluated typed AND term; short-circuit stops remaining terms",
      "candidateVisitUnit": "one known raw source examined or one possible initial-absent slot considered",
      "graphChecks": Json(result.graph_checks),
      "graphLimited": Json(result.graph_limited),
      "predicateEvaluations": Json(budget.evaluations),
      "candidateVisits": Json(budget.candidates),
      "predicateLimited": Json(budget.limited),
      "limits": {
        "transactions": 128,
        "operations": 4096,
        "edges": 50000,
        "graphChecks": Json(result.graph_limit),
        "predicateEvaluations": Json(budget.max_evaluations),
        "candidateVisits": Json(budget.max_candidates),
      },
    },
  }
  v4_json_copy(record)
}

///|
pub fn verify_relation_predicate_graph_v4(
  history : HistoryV4,
  supplied : Json,
  max_graph_checks~ : Int32 = 1000000,
  max_predicate_checks~ : Int32 = 1000000,
  max_candidate_visits~ : Int32 = 100000,
) -> Bool {
  match
    build_relation_predicate_graph_v4(
      history,
      max_graph_checks~,
      max_predicate_checks~,
      max_candidate_visits~,
    ) {
    Ok(result) => supplied == relation_predicate_graph_v4_json(result)
    Err(_) => false
  }
}