Skip to main content

Module budget

Module budget 

Source
Expand description

Deduplication, conflict, deterministic order, and what the budget could afford.

The three happen in that order and none of them re-reads the repository. What arrives is the Selection — one candidate per canonical identifier, already carrying every path that reached it — and what leaves is the answer, with an entry for everything kept and a reason for everything not.

§Why deduplication is two rules and not one

Structural duplication is the same identifier reached several ways, and it is already folded by the time this module runs: the interesting output is the record that it happened, so that “why is this ADR listed once when four things point at it” is answerable. Semantic duplication is two different identifiers standing for one thing — one file indexed under two kinds, or one rule at two versions — and that has to be decided, because keeping both would spend the budget twice on one fact and would leave the session holding two answers.

§First fit, not truncation

Entries are spent in one total order — tier, then relevance descending, then canonical identifier — and an entry that does not fit is excluded while the walk continues. A budget that stopped at the first thing too large would let one big file hide a hundred small relevant ones; first fit spends the ceiling and names everything it skipped, with what it would have cost, so raising the budget is an informed decision.

That last paragraph is the one worth seeing run, because the difference between first fit and truncation is invisible in a report and total in effect. Below, the most relevant candidate costs twice the whole ceiling: a truncating budget would have stopped there and answered with nothing, and this one skips it, says what it would have cost, and carries on to spend the ceiling on what does fit.

use std::collections::{BTreeMap, BTreeSet};

use majordomus_cli::devcontext::budget::spend;
use majordomus_cli::devcontext::select::{Candidate, Selection};
use majordomus_cli::devcontext::{Discovery, ExclusionReason, Selector, Tier};
use majordomus_cli::synthetic::SyntheticRepository;

let repo = SyntheticRepository::small().unwrap();
let index = repo.index().unwrap();

// `bytes` as a fact rather than a file: the cost is the one thing this example pins
let candidate = |uri: &str, relevance: f64, bytes: u64| Candidate {
    uri: uri.into(),
    kind: "knowledge".into(),
    title: None,
    tier: Tier::Knowledge,
    relevance,
    depth: 1,
    discovered_by: vec![Discovery {
        selector: Selector::Relation,
        reason: "reached along a declared edge".into(),
        via: None,
        edge: Some("depends_on".into()),
        depth: 1,
        confidence: 1.0,
        weight: 0.9,
    }],
    object: None,
    path: None,
    status: None,
    facts: BTreeMap::from([("bytes".to_string(), bytes.to_string())]),
};

let mut candidates = BTreeMap::new();
// 8000 bytes is 2000 tokens: twice the ceiling, and the more relevant of the two
candidates.insert("big".to_string(), candidate("majordomus://knowledge/big", 0.9, 8_000));
candidates.insert("small".to_string(), candidate("majordomus://knowledge/small", 0.5, 400));
let selection = Selection { candidates, unresolved: Vec::new(), diagnostics: Vec::new() };

let (s, _) = spend(&index, selection, &BTreeSet::new(), 1_000, &BTreeMap::new());

// the walk did not stop at the thing that would not fit
let kept: Vec<&str> = s.selected.iter().map(|e| e.uri.as_str()).collect();
assert_eq!(kept, ["majordomus://knowledge/small"]);

// and what it skipped is named, with what it would have cost
assert_eq!(s.excluded.len(), 1);
assert_eq!(s.excluded[0].uri, "majordomus://knowledge/big");
assert_eq!(s.excluded[0].reason, ExclusionReason::Budget);
assert_eq!(s.excluded[0].cost_tokens, 2_000, "so raising the ceiling is informed");

// the ceiling was respected, and `used` is the sum of what is in the answer
assert_eq!(s.budget.used_tokens, 100);
assert_eq!(s.budget.remaining_tokens, 900);
assert!(!s.budget.over_budget);

Structs§

Spend
Everything the budget decided, as four lists and the arithmetic behind them: what the session is given, what it is not and why, what collapsed into what, and what disagrees with what.

Functions§

spend
Compile the selection into the answer: fold, decide, order, spend.