# Decision-engine scope: governed adapters for selected problems

> **Current capability:** `company_decision_solve` supports two decision
> problems: inventory/service-level policy and rolling-horizon re-optimization.
> It records caller inputs as assumptions, evaluates and exposes candidate
> policies, and returns reviewable proofs. The rolling loop returns forecast
> calibration measures. Proposed actions are not executed; they carry the
> approval type required for a governed action. The engine does not read company
> data itself.
>
> **Ambition:** Connect governed enterprise context and measured outcomes to a
> broader set of decision workflows, so state, uncertainty, alternatives,
> constraints, actions and results can inform later decisions. A complete
> enterprise semantic layer, broad cross-domain scorecards, and proven general
> improvement to later decisions are not established capabilities.

Company OS is the substrate a decision engine runs inside, not the engine.
This document records what the `company_decision_solve` seam provides, which
decision problems have a real engine behind them, and how the remaining
catalog problems attach without a second governance model.

## Catalog entry (use this wording)

> **Decision-intelligence engines** — hostable as governed adapters through
> `company_decision_solve`: caller inputs recorded as assumptions, deterministic
> uncertainty sampling, every candidate evaluated, proposed actions tagged with
> the approval action type they need (`spend`), replayable `input_digest`.
> Shipped engines: inventory / service-level policy (#1) and rolling-horizon
> re-optimization with forecast calibration (#18). The other sixteen catalog
> problems have no engine yet; they attach by adding a `ProblemKind`.

## What the seam guarantees

| Property | Mechanism |
|---|---|
| Assumptions are visible | Every caller field is echoed in `proof.assumptions` with `source: caller_assumption`; the rolling loop marks fields it updated as `caller_assumption_updated_by_observations`. The engine never reads company data itself. |
| Replayable | `proof.input_digest` is SHA-256 over canonical JSON of the problem; identical inputs produce an identical proof (sampling is SplitMix64 seeded from the digest, or stratified inverse-CDF). |
| Reviewable | `proof.candidates` lists every policy evaluated, not only the winner; `proof.sensitivity` re-scores the chosen policy at ±10 % demand; `proof.decision.flags` names `capacity_bound` / `target_service_level_unmet`. |
| Proposes, never executes | `proof.proposed_actions[*].executed` is always `false`; each carries `approval_action_type` (`spend` for purchase orders) so the run raises the matching approval request instead of acting. |
| Read-only tool | `company_decision_solve` is classified `read_only` in `src/tools/mod.rs`; it needs no mutation scope and writes nothing. The run's tool-call receipt is the record. |

## Engines

### `inventory_policy` — demand, inventory and service-level (#1)

Input: per-period demand model (`normal`, `log_normal`, or `empirical`
samples), lead time, review period, unit cost, holding and shortage cost per
unit, optional `target_service_level`, on-hand / on-order, optional
`capacity_max_units`.

Method: order-up-to (base-stock) level over the protection interval
(lead time + review period). Demand over the interval is sampled
deterministically; candidates on a service-level grid plus the critical
fractile `p/(p+h)` are scored by sample average for expected holding units,
expected shortage units, cycle service level, fill rate, and cost. The
lowest-cost candidate wins, or the cheapest meeting the target when one is set.

Output decision: `order_up_to`, `order_quantity_now`, service level, fill
rate, expected cost, `working_capital_at_policy`, and one proposed
`purchase_order` when the inventory position is below the policy.

Verified: `crates/company-os-decision-engine` tests pin the normal quantile,
check the chosen level against the analytic newsvendor solution, and cover
target override, capacity cap, determinism, log-normal and empirical models.

### `rolling_horizon` — autonomous re-optimization (#18)

`start` makes the first decision and places the order in a pipeline. Each
`observe(realized_demand)` scores the forecast the last decision rested on
(mean, p10, p90), advances arrivals and consumption, records shortage, updates
the demand model (exponential smoothing for parametric models; window append
for empirical), re-solves, and returns the next proof with `calibration`:
observation count, mean absolute percentage error, bias, and 80 % interval
coverage. The state is a JSON value (`company_os.decision_rolling_state.v1`)
the caller carries between calls; it round-trips and replays identically.

Calibration is the evidence a reviewer uses to decide whether the loop should
keep being trusted; a persistently positive bias or low coverage is the signal
to stop auto-proposing and escalate.

## Inventory decision pilot (capability under evaluation)

The pilot proves the Decide → Act → Measure → Learn → Re-solve loop on one
item against one baseline before any other engine is added. It does not show
that the loop improves decisions; it produces the evidence a reviewer needs to
judge that.

| Step | Mechanism |
|---|---|
| Decide | `pilot::start` / `pilot::observe` solve and open a `DecisionRecord` (`company_os.inventory_pilot_decision_record.v1`) |
| Act | `pilot::record_action` stores what was executed. Any order needs `approval_ref` and `action_ref`; a quantity that differs from the recommendation needs `override_reason` and counts as an override. The rolling pipeline carries the executed quantity, not the recommended one. |
| Measure | `pilot::observe(realized_demand)` closes the record with both arms: the engine as operated, and the predeclared baseline replayed on the same demand |
| Learn | the rolling demand model updates from the observation, and calibration is reported |
| Re-solve | the next record opens from the updated model and position |

Each record carries:

- the **source snapshot**: source ref, content digest, as-of, and the demand
  and period definitions;
- the **definitions version** (`company_os.inventory_pilot_definitions.v1`);
- the full proof: assumptions, uncertainty summary, every candidate, and
  constraint flags;
- the baseline order;
- a `decision_digest` over everything known at decision time;
- the action evidence;
- the realized outcome.

The baseline (`fixed_order_up_to`), the holding-cost rate, the source snapshot,
and the minimum period count are fixed at `start` and bound by `setup_digest`.
A state whose setup was edited afterwards is refused.

`pilot::scorecard` compares the arms on the predeclared measures:

- fill rate;
- cycle service level;
- shortage cost;
- holding cost;
- total cost;
- average working capital (on-hand stock at unit cost);
- the override rate, which is reported for the engine arm only.

Before `min_periods_for_comparison` it compares nothing. The scorecard always
reports `earned_claim_level: none` and `status: capability_under_evaluation`:
it covers one item and one run, has no uncertainty interval, and no producing
code may raise its own claim level ([claim
contract](./DECISION_LEARNING_CLAIM_CONTRACT.md) §6).

`company_os_core::inventory_pilot_episode` maps each record onto a [Decision
Episode](./DECISION_EPISODE_CONTRACT.md) without writing anything:

- the candidates, the baseline, and an operator-set option become the sealed
  alternatives;
- `decision_digest` becomes `context_digest`;
- every pilot measure, plus its `baseline_` counterfactual, is a predeclared
  expected outcome;
- a followed recommendation resolves as `automated`, with the approval as its
  authority;
- an override becomes an `override` intervention plus a `human` resolution;
- the period's outcome is observed against
  `inventory_pilot_baseline:<setup_digest>`. Engine-arm measures are `derived`
  and baseline measures `estimated`.

Callers submit the `OpenDecision` and commands through the authenticated
episode HTTP adapter.

The Python pilot preflight (`scripts/company_os_decision_learning_pilot.py`) was
not changed here. Its fixed metric set does not yet include the inventory
measures.

Agents drive the pilot through `company_decision_solve` with `problem:
inventory_pilot` and a `pilot_action`:

- `start` takes `inventory` and `pilot_setup`.
- `record_action` takes `state` and `action`.
- `observe` takes `state` and `realized_demand`.
- `scorecard` takes `state`.

Each response (`company_os.inventory_pilot_response.v1`) returns the next
`state`, the `open_record`, and the `scorecard`. `observe` also returns the
`closed_record` it measured. The tool stays `read_only`: `record_action` stores
what the caller reports was executed elsewhere under its approval.

Not wired yet:

- Nothing posts pilot episodes automatically.
- No pilot has run on real company data.

Verified: `crates/company-os-decision-engine/tests/pilot.rs` (including the
JSON entry point `pilot::handle`) and
`crates/company-os-core/tests/inventory_pilot_episode.rs`. The tool file
`src/tools/decision_engine_tools.rs` and its tests were compiled and run
against the real engine in a standalone harness, with `Tool`/`ToolOutput`
copied from `src/tools/mod.rs`. A full root-crate build has not been run.

## Not shipped

Production planning and scheduling, procurement and supplier mix,
multi-echelon network flows, maintenance policy, workforce allocation,
portfolio and capex prioritization, experiment selection, order allocation,
working-capital coordination, pricing and promotion, clinical and energy
dispatch, and investment sizing have no engine. Each needs its own model and
usually a solver; they attach by adding a module and `ProblemKind` to
`crates/company-os-decision-engine` so the proof shape, digest, and
proposed-action contract stay the same.

## Adding a problem

1. Add a module with a `Problem` type (`deny_unknown_fields`, a `validate`),
   a deterministic sampler seeded from `input_digest`, and a `solve` that
   fills `DecisionProof` — every candidate, flags, sensitivity, proposed
   actions with an approval action type.
2. Add the `ProblemKind` variant and a branch in
   `src/tools/decision_engine_tools.rs`.
3. Prove at least one analytic case and a determinism case in the crate.
