Skip to main content

rac_engine/
derived.rs

1//! The derived read-model for one corpus snapshot (ADR-099/ADR-103).
2//!
3//! Port of `services/derived_cache.py` `build_derived_index`: one walk feeds
4//! every structure, each a pure function of the sorted-path snapshot, so the
5//! whole bundle is content-addressable and the persisted store reproduces a
6//! fresh build byte-for-byte (spec/index-contracts.json `derived_cache`).
7
8use serde_json::Value;
9
10use crate::relationships::{corpus_items, relationships_from_corpus, CorpusItem, Relationship};
11use crate::resolve::{entry_from_item, field_tokens_of, is_live_decision, FieldTokens, IndexEntry};
12use crate::retrieve::{scope_rows_from_items, ScopeRow};
13
14/// The bundle schema version (`derived_cache.SCHEMA_VERSION`).
15pub const SCHEMA_VERSION: &str = "3";
16
17pub(crate) const DECISION_TYPE: &str = "decision";
18
19/// The expensive derived structures for one corpus snapshot.
20pub struct DerivedIndex {
21    /// Repository index rows in walk (sorted-path) order — docid order.
22    pub index_entries: Vec<IndexEntry>,
23    /// Per-entry BM25F field-token vectors, parallel to `index_entries`.
24    /// (The oracle keys by path; docid order carries the same information
25    /// without re-keying, and paths are unique within a walk.)
26    pub field_tokens: Vec<FieldTokens>,
27    pub relationships: Vec<Relationship>,
28    pub live_decision_paths: Vec<String>,
29    /// The `get_summary` portfolio dict (ADR-103) — the JSON payload the
30    /// store persists verbatim in `portfolio.seg`.
31    pub portfolio_summary: Value,
32    pub scope_rows: Vec<ScopeRow>,
33}
34
35/// Build the derived structures from an already-walked corpus snapshot.
36pub fn build_derived_index_from_items(
37    directory: &str,
38    items: &[CorpusItem],
39    recursive: bool,
40) -> DerivedIndex {
41    // Resolve the graph once; inbound degree is counted off the resolved
42    // edges exactly as `inbound_counts_from_relationships` does.
43    let relationships = relationships_from_corpus(items);
44    let mut inbound: std::collections::HashMap<&str, i64> = std::collections::HashMap::new();
45    for rel in &relationships {
46        if let Some(resolved) = &rel.resolved_path {
47            *inbound.entry(resolved.as_str()).or_insert(0) += 1;
48        }
49    }
50    let index_entries: Vec<IndexEntry> = items
51        .iter()
52        .map(|item| {
53            entry_from_item(item, inbound.get(item.path.as_str()).copied().unwrap_or(0))
54        })
55        .collect();
56    let field_tokens: Vec<FieldTokens> = index_entries.iter().map(field_tokens_of).collect();
57    let live_decision_paths: Vec<String> = items
58        .iter()
59        .filter(|item| {
60            item.spec.map(|s| s.name == DECISION_TYPE).unwrap_or(false)
61                && is_live_decision(&item.artifact)
62        })
63        .map(|item| item.path.clone())
64        .collect();
65    let summary = crate::portfolio::portfolio_from_corpus(directory, items, recursive);
66    DerivedIndex {
67        index_entries,
68        field_tokens,
69        relationships,
70        live_decision_paths,
71        portfolio_summary: crate::output::portfolio_summary_value(&summary),
72        scope_rows: scope_rows_from_items(items),
73    }
74}
75
76/// Build the derived structures fresh from one corpus walk (the miss path).
77pub fn build_derived_index(directory: &str, recursive: bool) -> DerivedIndex {
78    let items = corpus_items(directory, recursive);
79    build_derived_index_from_items(directory, &items, recursive)
80}