# Budget Manager v1 This milestone implements deterministic accounting for the initial hypotheses. It does not run investigation tools, invoke an LLM for evidence assessment, build diagnoses, or start an investigation loop. `docs/architecture.md` remains the authoritative specification, particularly sections 16, 18, 19, 25 and 31. ## Configuration and deployment tuning Copy `config/budget.example.toml` to the ignored `config/budget.local.toml` and load it with `BudgetConfig.load(path)`. This is a separate config from the initial hypothesis allocations in `config/analysis.example.toml`. These are conservative starting values, not format constants or empirically calibrated scoring rules: | Setting | Default | Meaning | | --- | --- | --- | | `default_action_cost` | 10 | Positive integer cost for an action without an override | | `action_costs` | empty in Python | Per-action positive integer costs; example TOML includes future tool names | | `max_reward_bps` | 8000 | Maximum refund in basis points (8000 = 80%); range 0–9999 | | `new_evidence_score` | 100 | Deterministic score for unseen, nonempty evidence; range 0–100 | | `low_information_threshold` | 20 | Information scores at or below this value count as low gain | | `max_low_information_steps` | 3 | Consecutive low-gain settlements before abandonment | The example config assigns `source.read_context` a cost of 5, `source.find_text` and `database.lookup` 10, and `research.lookup` 20. These names describe accounting overrides only: they neither implement tools nor authorize arbitrary operations. The future Tool Router must independently validate every action. The complete effective policy is frozen in the first budget event of an analysis revision. Changing the config cannot alter an existing policy, refill balances, undo expenses, or reopen terminal accounts. An existing snapshot or identical operation can be replayed with a different config; new operations reject with `BUDGET_POLICY_CHANGED`. Use the frozen config to continue an existing revision; new revisions can adopt the tuned config. Every event includes the effective policy, so historical accounting remains reproducible after deployments change. Tune costs, refund ceiling, novelty score, threshold and consecutive-step limit using actual result quality and cost data. The initial hypothesis allocation remains separately configurable. No automatic online adaptation is implemented. The invariants below deliberately cannot be disabled by configuration. ## Calculation and invariants The unit is `investigation_units`, unrelated to LLM token counts, money or provider retry counts. The LLM can supply only a bounded semantic score and a reason through the future trusted controller. It cannot choose actual costs, refunds, remaining balances, or budget policy. For each completed action: ```text information = min(deterministic_novelty, semantic_score) reward = floor(cost * max_reward_bps * information / 1_000_000) reward = min(reward, cost - 1) remaining = initial - total_spent + total_rewarded ``` Both scores range from 0 to 100. Full configured reimbursement requires both scores to reach 100. A score is an ordinal accounting input, not a confidence percentage. For example, cost 10 and both scores 100 produce reward 8 under the default policy; a semantic score of 50 produces reward 4. Even the most generous allowed policy always has `0 <= reward < cost`: each settled action consumes at least one whole unit. Thus a hypothesis cannot fund infinite investigation. Novelty v1 hashes canonical JSON evidence (sorted object keys, preserved array order). New nonempty evidence gets `new_evidence_score`; empty or already seen evidence gets zero. Deduplication spans hypotheses and prior analysis revisions in the same case package. Fresh revision budgets do not make old evidence novel. Transport metadata and query IDs should be kept outside the evidence payload. This detects exact normalized content reuse, not semantic equivalence. Future EvidenceStore/scoring work may provide richer deterministic novelty signals; this milestone does not pretend to assess factual correctness or entailment. The controller supplies JSON evidence and safe references to existing package sources. Source hashes, evidence, computed novelty, semantic assessment reason, cost and reward are all retained. Accounting checks references and integrity, not whether an assessment is scientifically justified. Tests simulate assessments; no live model or paid provider is used. ## Durable controller API ```python from viano_bug_analysis.budget import BudgetConfig, BudgetManager budget = BudgetManager(store, BudgetConfig.load("config/budget.local.toml")) budget.initialize(case_id, package_id) budget.reserve(case_id, package_id, "H1", "query-0001", "source.read_context") # A future controller invokes the allowed read-only tool after reservation. budget.record_result( case_id, package_id, "H1", "query-0001", {"finding": "Example stored observation"}, source_refs=[source_ref], ) # In the next iteration the controller validates a bounded semantic assessment. result = budget.settle( case_id, package_id, "H1", "query-0001", semantic_score=50, reason="Example assessment of this stored result", ) ``` - `initialize` verifies a completed current-revision hypothesis batch, registry, allocations, context and original successful LLM response. It creates equal balances from the controller-owned allocations; it cannot allocate more IDs. - `reserve` durably debits the configured cost before external work. Unknown hypotheses, blank IDs, unaffordable costs and terminal accounts are rejected. Only one unsettled action per hypothesis is allowed. Different hypotheses can have their own pending actions; action IDs are scoped to revision/hypothesis. - `record_result` persists evidence, source references and their hashes. It changes the action to `awaiting_assessment`, with no immediate reimbursement. - `settle` accepts only an integer score 0–100 and a nonblank reason. The score is bound to the already stored action result, and the manager computes the reward. - `fail` closes a reserved action without reimbursement. Failed or uncertain executions still cost budget and count as low information. A pending action survives restart and blocks another reservation; the manager never silently cancels it or retries external work. The controller decides whether the same external operation can safely resume or must be closed with `fail`. - `reject` marks an account terminal without refunding or deleting its history; a pending action must first be settled or failed. - `snapshot` validates/replays the ledger without changing budgets. Identical repeated operations return the current package/state with `reused=True`. Changed inputs under the same operation/hypothesis/action identity are rejected. Repeated initialization cannot refill balances, including after index-write failure, service restart, transport under another filename or config changes. `BudgetResult` returns the package path, revision, event reference and current state. Its `investigable` property lists nonterminal accounts without pending actions; `ready_for_evaluation` is true only when all accounts are terminal and no action awaits completion. An empty investigable list alone is not sufficient while asynchronous actions are pending. ## Status and lifecycle boundaries Budget account status is separate from qualitative hypothesis confidence and the immutable hypothesis birth/creation registry. The manager owns `new`, `investigating`, `budget_exhausted`, `abandoned_low_information_gain` and the controller-requested `rejected` budget eligibility marker. It does not infer `supported`/`plausible`, diagnose causes, create replacements or restore slots. The birth document's `budget_accounting = not_started` describes generation time and remains immutable; `artifacts.budget_ledger` is authoritative for the subsequent accounting state. The cheapest cost in the configured override/default table is the affordability floor. Immediately after initialization and after settlement, a balance below that floor becomes `budget_exhausted`. Pending actions are not marked exhausted before their possible reward is evaluated. If affordable but the consecutive low-gain limit is reached, the account becomes `abandoned_low_information_gain`. A higher information score resets that counter. Exhaustion takes precedence if both conditions occur. Request-specific insufficient funds reject that request; a cheaper permitted action can still be considered. Mutations are allowed only in `GENERATING_HYPOTHESES` or `INVESTIGATING_HYPOTHESES`. Budget initialization does not start investigation or transition the case. An exhausted set exposes eligibility for final evaluation, but does not create a diagnosis or route the case to Human Review. Those decisions belong to the future controller/evaluator. Reopening retains old artifacts and starts accounting from the new revision's accepted allocations. ## Persistence, validation and limits Each operation appends one immutable event/snapshot: ```text analysis/budgets/0001/events/00000001.json # policy + initial balances analysis/budgets/0001/events/00000002.json # reservation analysis/budgets/0001/events/00000003.json # result analysis/budgets/0001/events/00000004.json # assessment + computed reimbursement ``` The manifest's `artifacts.budget_ledger` points to the latest event. Each event has format/version, identities, original hypothesis/registry hashes, sequence, parent/hash, effective policy, request and fully derived accounting state. The existing CheckpointStore publishes event, checkpoint and pointer atomically, increases `package_generation`, preserves unknown files and checks the prepared generation. A per-case budget lock prevents duplicate dispatch; short existing case/worker locks protect publication. No lock waits on an external tool or LLM. Replay verifies event schemas, checkpoint bindings, immutable origins, lineage, source hashes and every computed state. Pointer deletion, changed allocations, costs, rewards, balances or broken lineage are controlled errors, never a reset. Checks detect inconsistent artifacts; hashes are not digital signatures and do not authenticate a maliciously rewritten complete case history. `budget-policy-v1`, `budget-request-v1` and `budget-event-v1` JSON schemas are bundled with the Python package. Byte/event limits in `BudgetLimits` are adjustable operational safeguards, independent of format v1: defaults are 2 MiB per event, 1 MiB per evidence payload and 10,000 events across retained budget revisions. Limits reject explicitly rather than truncating history or dropping expenses. Snapshots contain cumulative state, so long-lived cases may need larger limits; incremental replay optimizations are a later performance task. The CaseAssembler excludes checkpoint-declared budget events from raw case input identity, preserving replay of earlier phases. Undeclared/unknown files remain preserved and part of raw input inventory as before. For asynchronous external testing, see [async-tests-v1.md](async-tests-v1.md). `BLOCKED` pauses the case, not accounting history: balances and pending reservations survive unchanged. Confirmed feedback resumes the same revision; only subsequent result assessment changes its budget. Human/automated tests have separately configurable explicit costs, with no idle-time charges.