# Utopia Pricing Agent โ Architecture (engineering detail)
The technical reference: data sources, decision cascade, formulas, file map, backtests.
For the plain-English overview see **[ARCHITECTURE.md](ARCHITECTURE.md)**.
A one-page Streamlit dashboard (Utopia/CRAI design system) that turns **live COSMOS
data** into price recommendations a human can Approve / Modify / Reject. Read-only:
nothing is written back to COSMOS or Amazon.
---
## 1. High-level view
```mermaid
flowchart LR
U["๐ง User
(same-network browser)"] --> APP
subgraph APP["app.py โ presentation (Streamlit, CRAI theme)"]
SIDE["Sidebar
Single product / Product line
+ filters, kill switch"]
QUEUE["Recommendation queue
tiles ยท pills ยท rows"]
SECT["Per-SKU sections
price ยท inventory ยท scenarios
competitors ยท PPC ยท costs ยท AI"]
end
subgraph DASH["dashboard/ package"]
THEME["theme.py
CRAI tokens + plotly template"]
LIVE["live_data.py
adapter + decision engine
+ scenario economics"]
end
subgraph CORE["src/pricing_agent โ analysis core"]
AN["analyze.py"]
MARGIN["margin_engine.py"]
ELAST["elasticity.py"]
PERF["performance.py"]
SVC["cosmos/service.py"]
CLIENT["cosmos/client.py"]
end
COSMOS[("COSMOS API")]
APIFY[("Apify โ optional")]
APP --> THEME
APP --> LIVE
LIVE --> AN
AN --> MARGIN & ELAST & PERF
AN --> SVC --> CLIENT --> COSMOS
LIVE --> SVC
AN -.optional.-> APIFY
```
---
## 2. Layers
| Layer | Files | Responsibility |
|---|---|---|
| **Presentation** | `app.py` | All rendering, zero pricing logic. Session state (approve/modify/reject, filters, per-SKU section + window), staged progress loader, session-state cache. |
| **Design system** | `dashboard/theme.py`, `.streamlit/config.toml` | CRAI palette (cream `#f4f0e8`, teal `#0c8276`, coral `#df4f33`, navy `#22304e`), Inter font, plotly template. |
| **Adapter + engine** | `dashboard/live_data.py` | Builds the per-SKU dict; **decides** the action; computes **scenario economics** (elasticity projection โ bulk reconciliation โ calibration); exposes `scenarios_for_window()`. |
| **Competitive state** | `src/pricing_agent/competitive_state.py` | The one competitive fact the cascade may read. Adapts either scraper into a typed `WON`/`LOST_PRICE`/`LOST_ELIGIBILITY`/`SUPPRESSED` state with a source and a timestamp, gates it on age, and logs disagreement between sources. |
| **Analysis core** | `src/pricing_agent/analyze.py` | Orchestrates one SKU: fees โ trend โ bulk โ ad cost โ elasticity โ actual-profit evidence. |
| **Money math** | `tools/margin_engine.py` | Pure: break-even, MAP, contribution margin, suggested price. |
| **Statistics** | `elasticity.py`, `performance.py` | Log-log elasticity fit, profit-optimal sweep, actual-profit aggregation. |
| **Data access** | `cosmos/{client,service,models}.py` | Auth + retry client; endpoint calls + response flattening; typed pydantic models. |
---
## 3. Data sources โ what each COSMOS endpoint feeds
```mermaid
flowchart TB
subgraph COSMOS["COSMOS API"]
TH["/sales-insight/takehome-calculator
nested fees.breakdown ยท cost.breakdown"]
INVP["/invp-insight
trend + inventory + dateMap PROJECTIONS"]
BULK["/sales-insight/bulk-calculator
storage + total take-home"]
SI["/sales-insight (daily, 6-month)
price ยท units ยท revenue ยท profit ยท ad spend"]
PROD["/products
brand ยท marketplace"]
CAMP["/api/campaigns ยท /adsApi
budget ยท ACoS ยท ad sales (not yet wired)"]
end
TH -->|"_flatten_takehome()"| FEES["Fee model
referral% ยท FBA ยท landed ยท returns"]
INVP --> TREND["Velocity + cover days"]
INVP --> INVPROJ["Inventory Outlook tab
real weekly units/value/cover/arrivals"]
BULK --> STORAGE["Storage + take-home (scenarios)"]
SI --> HIST["180-day daily series
(window filter + calibration)"]
SI --> ADS["Ad spend / TACoS (PPC tab)"]
PROD --> META["Brand / marketplace"]
```
**Three response quirks handled:**
- **Fees come nested** (`fees.breakdown["Referral Fee"]`, `"$ 9.28"` strings).
`service._flatten_takehome()` normalises them โ without it every fee parsed to 0 (the
old "$0.99 / break-even $0" bug).
- **INVP `dateMap`** holds COSMOS's own **forward inventory projection** (weekly units,
value, cover days, warehouse arrivals). The Inventory tab renders this directly โ not
a locally-invented forecast.
- **SKU lookups are CONTAINS/relevance searches, not equality.** See ยง3.1 โ this one
silently bound the wrong product's data to a SKU.
### 3.1 Exact-SKU joins (`service._exact_row`)
Neither `/api/products` nor `/api/invp-insight` has an equality filter, and both used to
fall back to `data[0]` "if COSMOS returned a fuzzy match set". That bound one product's
ASIN, cost, brand and inventory to a **different product's SKU**.
Observed live: `get_product("UBCFKFITTEDSHEETWHITECALKING")` โ a SKU COSMOS does not carry
at all โ returned `UBMICROFIBERGUSSETPILLOWWHITEQUEEN` / `B08DTH86Q2`.
Two independent problems, and each guard is necessary:
1. **Wrong parameter.** `get_product` queried `q=` (relevance across the whole catalogue).
For `UBMICROFIBERDUVETTWINWHITE` the correct row sat on **page 2 of 100-row pages**,
behind 100 unrelated products โ a 20-row lookup never saw it. `sku=` returns it first.
2. **`sku=` is still a CONTAINS filter.** `sku=UBCFKMATTRESSPROTECTORTWIN88` returns three
rows: the real one (`B00MRH9NCK`), the `...BOX` variant (`B09K7HXJ4M`), and a
`WAL...` Walmart row whose "ASIN" (`8946709597`) is not an ASIN. **All three are
`marketplace: AMAZON_USA`**, so the marketplace check alone does not separate them โ
only the exact SKU test does.
`_exact_row()` requires an exact SKU match **and** the right marketplace, and returns
`None` otherwise, logging what it rejected. Blast radius of the old behaviour:
`analyze_price` assigns `asin = product.asin` whenever INVP has none, so the competitive
scrape would have run against an unrelated listing; and `get_invp`'s `skuPrefix` matches
every colour variant, so a sibling's inventory and cover days fed `LOW_STOCK` /
`EXCESS_STOCK` directly โ a sibling reading 12 units / 3 days would fire a stockout raise
on a SKU holding 4,000 units.
Genuinely **not** in this COSMOS integration โ shown as "โ", never faked: ad-attributed
sales / ACoS / campaign budget (live in `/api/campaigns` + `/adsApi`, not yet wired), and
historical competitor prices (Apify gives a current snapshot only).
**Competitor data is no longer display-only.** COSMOS has no Buy Box, no rival price and no
third-party offer anywhere in it โ that gap is filled by a scrape, and two of its facts
(our Buy Box being suppressed, and a rival materially undercutting us) reach the verdict.
They are the only two, they are bounded by the guardrails, and their absence changes
nothing. See ยง6.
---
## 4. Per-SKU pipeline (one "Analyze")
```mermaid
sequenceDiagram
participant U as User
participant A as app.py
participant L as live_data.build_live_sku
participant AN as analyze.py
participant S as CosmosService
U->>A: Single product / Product line
A->>L: get_live_data(skus, progress_cb)
Note over A: staged progress bar (3โ10โ45โ82โ94โ100%)
L->>S: get_current_price
L->>S: get_sales_history (180d, parallel windows)
L->>AN: analyze_price (fees, trend, bulk, elasticity, evidence)
L->>S: get_invp (real inventory projection)
L->>S: bulk_quote (storage)
L->>L: decide action + scenario economics + 30d/6mo calibration
L-->>A: {summary, details, errors} (session-cached)
A-->>U: queue + expandable per-SKU analysis
```
---
## 5. Scenario economics (the heart of the Scenarios tab)
For each candidate price, one consistent chain:
```mermaid
flowchart LR
W["Window filter
7/14/30/90d ยท 6mo"] --> BASE["Baseline velocity
= avg units/day in window"]
BASE --> DEMAND["Units(p) = units ร (p/cur)^elasticity"]
DEMAND --> REV["Revenue = units ร p ร 30"]
REV --> AD["Ad spend = TACoS ร revenue"]
DEMAND --> TH["Take-home (bulk calculator fee model)"]
TH --> GROSS["Gross = take-home โ storage โ ad"]
GROSS --> CAL["ร realization factor
(actual booked รท modeled at current)"]
CAL --> NET["Net profit / 30d"]
```
Key rules:
- **Current row = FACT**, not a projection: real units, real revenue, real ad spend, real
booked profit. Its price is the **average price sold** (revenue รท units) so
`price ร units ร 30 = revenue` reconciles โ this is *below* list when promos ran, and
changes with the window because the avg selling price differed period to period. The
**list price is fixed**.
- **Calibration:** raw bulk-calculator profit over-states reality (prices at list, ignores
real returns/promos). A **realization factor** = actual booked profit รท modeled profit at
the current price scales every projected row.
- **The elasticity used for projections is gated** (`projection_elasticity()`). It honours
the same `actionable` test the decision does: a slope whose 95% CI spans zero โ or a
**positive** slope, which `estimate_elasticity` can return since `actionable` requires
`e < 0` but the value is not clamped โ falls back to `FALLBACK_ELASTICITY`. Ungated, a
positive slope projected that *raising* price sells *more*, and that number drives the
30-day impact tile, the portfolio opportunity total and the queue sort.
`elasticity_is_fitted` records which was used.
- **โญ** marks the highest-net-profit price in the current view.
---
## 6. Decision engine (deterministic, first match wins)
```mermaid
flowchart TD
S([signals]) --> R0{cost data = 0?}
R0 -- yes --> INV["๐ INVESTIGATE ยท NO_COST_DATA"]
R0 -- no --> RB{our Buy Box suppressed?}
RB -- yes --> INVB["๐ INVESTIGATE ยท BUYBOX_SUPPRESSED"]
RB -- no --> R1{price < break-even?}
R1 -- yes --> UP1["โ raise to safe floor ยท BELOW_BREAK_EVEN"]
R1 -- no --> R2{losing money after ads?}
R2 -- yes --> UP2["โ raise ยท LOSING_MONEY"]
R2 -- no --> R3{cover โค 35d?}
R3 -- yes --> UP3["โ +5% ยท LOW_STOCK"]
R3 -- no --> R4{โ30% sales, no cause?}
R4 -- yes --> INV2["๐ INVESTIGATE ยท UNEXPLAINED_DROP"]
R4 -- no --> R5{cover โฅ 90d?}
R5 -- yes --> DN["โ โ5% ยท EXCESS_STOCK"]
R5 -- no --> RC{rival โฅ3% below AND corroborated?}
RC -- yes --> DNC["โ toward rival ยท COMPETITOR_UNDERCUT"]
RC -- no --> R6{profit-optimal โ current?}
R6 -- yes --> MOVE["โ/โ toward optimal ยท PROFIT_OPTIMAL"]
R6 -- no --> R7{a price we ran booked more?}
R7 -- yes --> BO["โ/โ toward it ยท BEST_OBSERVED"]
R7 -- no --> HOLD["โ MAINTAIN ยท NO_SIGNALS"]
```
Guardrails: floor = highest of four break-evens ร 1.02, ceiling = current ร 1.25; the
recommended move is capped at ยฑ5% per step; the kill switch pauses all approvals.
### AD_SPIRAL โ applied after the cascade
When ad cost per unit climbs almost as fast as price, each extra $1 of price buys only
cents of contribution and **no** price reaches break-even. That verdict (`AD_SPIRAL`,
Investigate-and-hold) is applied *after* the cascade and overrides whatever fired โ so
what it must **not** override is named explicitly:
```python
AD_SPIRAL_YIELDS_TO = frozenset({"NO_COST_DATA", "BUYBOX_SUPPRESSED", "LOW_STOCK"})
```
| Yields to | Why |
|---|---|
| `NO_COST_DATA` | With `costPerUnit`/`fbaFee` at 0, `fixed` is understated, which makes the `contribution <= 0` test **easier** to hit. A missing-COGS SKU would be sent to the ad console when the fix is a data-entry field. |
| `BUYBOX_SUPPRESSED` | A listing nobody can buy from has no meaningful ad economics. |
| `LOW_STOCK` | The only one that changes a **price**, not just a label. A shelf about to empty gets +5% whatever the ad slope does โ those units sell regardless, so the only question is what we get for them. Holding sells the last of the stock cheap. |
Deliberately narrow: `EXCESS_STOCK` does **not** outrank it, because cutting price to clear
stock is exactly the move that cannot work when ads eat the contribution.
### Competitor rules โ the two that can move a price, and what bounds them
Both branches read a single `CompetitiveState`
([competitive_state.py](src/pricing_agent/competitive_state.py)), never a raw scrape:
| Rule | Fires when | Effect |
|---|---|---|
| `BUYBOX_SUPPRESSED` | Amazon is not showing our offer | **Investigate, hold.** Placed directly under `NO_COST_DATA`: the only two states where the answer is "go and fix something" rather than "set a price". |
| `COMPETITOR_UNDERCUT` | cheapest rival โฅ `competitor_undercut_material_pct` below us, **and the basis qualifies** (below) | **Decrease toward the rival**, floored and step-capped like every other branch. |
**Basis decides what "qualifies" means**, because `competitor_min` can be a price of two
different things:
- `same-asin-buybox` (Apify) โ another seller's offer on **our own listing**. Only
`LOST_PRICE` fires; a rival holding the Buy Box *above* us is `LOST_ELIGIBILITY`, where
cutting donates margin. Losing the Buy Box on price **is** the corroboration.
- `like-for-like-sheet` โ a **rival brand's** equivalent variant, matched on size + colour.
This additionally requires **corroboration**: either demand has materially dropped, or we
are not actually winning the Buy Box.
*Why:* on its own a sheet row says only "a different brand is cheaper". True, reportable,
but not evidence the gap is costing us anything โ we can sit 3% dearer, hold our own Buy
Box and sell fine on brand, reviews or the Prime badge. And because this branch sits
**above** `PROFIT_OPTIMAL`, an uncorroborated cut could overrule an elasticity fit that
wanted a *raise*. Switch: `competitor_sheet_requires_corroboration` (default `true`).
The documented `UNEXPLAINED_DROP` fall-through is unaffected โ
`UBMICROFIBERDUVETKINGPURPLE` and `UBMICROFIBERBS4PCFULLGREY` arrive here *with* a
velocity drop, which is the first form of corroboration.
An uncorroborated material undercut is still **reported** in the root cause ("Competitor
undercut not acted on"), so it never looks like missing data.
A third signal, a **competitor premium** while we hold the Buy Box, is a narrative note
only. It never sets a price and never changes an action.
Ordering is deliberate: **inventory risk outranks competitor position, which outranks
profit-optimal.** Chasing a rival down while the shelf is emptying pays margin to sell out
faster.
Three properties make this safe to ship:
1. **Fail-safe.** Absent, failed, stale (> `competitor_state_max_age_hours`) and
"ownership unknown" all collapse to one flag, and the cascade then computes exactly the
verdict it computed before competitor data existed. Competitor data can only ever *add*
a verdict.
2. **Never below break-even.** The rival price is a *candidate* (`comp_match`), not a
decision.
3. **One named reason per verdict.** No blended scores โ every fired rule is traceable to a
single reason code, and `logger.info` names the SKU, the rule, the state and the source.
Thresholds live in [config/pricing_rules.yaml](config/pricing_rules.yaml)
(`competitor_undercut_material_pct: 0.03`, `competitor_premium_material_pct: 0.10`,
`competitor_state_max_age_hours: 6.0`, `competitor_sheet_requires_corroboration: true`),
not in code. The 3% floor sits above the ~2% band our own realized price already swings
through as coupons toggle.
### Inventory cover matches COSMOS Inventory Planning
`cover_days` **is COSMOS's own `coverDays`**, so the dashboard and the INVP grid never quote
two different numbers for one SKU. COSMOS counts **inbound** stock against a **7-day**
velocity, so it reads longer than what is on the shelf โ `UBMICROFIBERDUVETTWINWHITE` is
78 days on (3,999 on hand + 1,030 inbound) รท 64/day, against 63 on-hand-only. Both are
reported: the tile leads with the matched figure and appends `63 d on hand, rest inbound`.
The on-hand figure remains the **fallback**, because COSMOS returns `coverDays: 0` on some
very low-velocity SKUs that hold months of stock (`UBMICROFIBERBS4PCFULLGREY`: 167 units,
334 real days, COSMOS said `0`). Zero satisfies neither inventory rule, so taken literally it
silences both. The reconciled figure is computed **before** `_decide` and passed in, so the
tile and the rule that fired read the same number by construction.
**Trade-off, accepted deliberately:** stockout risk is now judged partly on stock that has not
landed. Measured over the 56-SKU covered line, matching COSMOS moved 7 verdicts โ
`LOW_STOCK` 7 โ 4, `EXCESS_STOCK` 18 โ 22. The one to watch is
`UBMICROFIBERBS4PCKINGWHITE`: **12 days on the shelf, 84 with inbound**, so it no longer
raises. If that shipment slips, nothing protects it.
Display bands are COSMOS's Alpha/Beta scheme (`theme.COVER_BANDS`, Alpha 20/40/70/100). The
pricing **triggers** are separate and live in `pricing_rules.yaml`
(`low_cover_days: 35`, `high_cover_days: 90`) โ COSMOS's pink at 70 days is a *replenishment*
warning, while crossing a trigger here spends margin on a 5% move.
### Coverage: the comparison sheet gates competitor data, one product line at a time
The competitor workbook currently covers **one product line**, so the engine reads it as the
first competitor source and **gates on coverage**:
| SKU | Competitor state |
|---|---|
| In the sheet | Priced from the sheet โ real like-for-like rival prices, `basis=like-for-like-sheet` |
| Not in the sheet | **`N/A`**, naming what the sheet *does* cover. No rule fires; the verdict is byte-identical to the competitor-blind one |
**Coverage is the exact SKU set in the sheet, not a line prefix.** Measured against the real
workbook, a prefix gate would be wrong in both directions: the `UBMICROFIBERDUVET` run contains
49 `UBMICROFIBERDUVET*` SKUs **and 7 `UBMICROFIBERBS4PC*`** ones, while the line has 139 SKUs
in COSMOS of which only 56 reached a comparison row. So the sheet's own SKU list is the
authority, and "not in the sheet" is a **coverage hole**, never a claim that the SKU has no
competitors.
`competitor_sheet_only: true` (default while one line is under test) means an uncovered SKU gets
N/A rather than falling through to a per-ASIN Apify scrape. Config: `competitor_sheet_path`
(blank = auto-discover the newest `Competitor_Price_Comparison_*.xlsx`), `competitor_sheet_dirs`,
`competitor_sheet_max_age_hours: 168` (the sheet is a 25โ35 min batch run, not a live feed).
Two sheet-driven refinements, both from real rows:
- **A rival whose own Buy Box is suppressed is excluded from the band.** Their price is not
buyable, so undercutting it donates margin for nothing.
- **A material undercut can explain a velocity drop** (subject to the corroboration rule
above), converting an `UNEXPLAINED_DROP` Investigate into an actionable verdict โ exactly
as the existing stockout branch already did.
### Two scrapers, one state
| Source | Authoritative for | Why |
|---|---|---|
| **Apify** (`tools/amazon/apify.py`) | **Buy Box state read by the engine** | The only source carrying a seller id, so the only one that can tell `WON` from `LOST_PRICE` from `LOST_ELIGIBILITY`. Those lead to opposite actions. |
| **Playwright** (`../scraper/`) | The workbook: like-for-like size/colour matching, BSR, demand buckets, SKU gaps | Apify cannot produce any of it. Its Buy Box field knows only whether a price *rendered*, not whose it was. |
The split is by **question**, not preference. `reconcile()` cross-checks the authoritative
state against the Playwright run's own cache (`scraper/.scrape_cache.json`, keyed `ASIN@ZIP`)
and **logs any disagreement**. A disagreement never changes the verdict.
---
## 6.1 Verdict backtest
`scripts/backtest_competitor_rules.py` re-runs the **captured real cascade inputs** (real
`AnalysisResult`, real 180-day history, real fee stack, real scenario grid) through the same
`_decide`, varying only the arm. Nothing is reconstructed by hand.
Competitor rules blind vs live (7 requested SKUs, 6 analysed โ see ยง9):
| Arm | Verdicts changed |
|---|---|
| Real competitor state | **0 / 6** โ the fail-safe working |
| Counterfactual 8% undercut | 4 / 6 โ the others blocked by `LOW_STOCK` / `LOSING_MONEY` |
| Counterfactual suppression | 6 / 6 โ Investigate |
Policy delta, old vs new (the two cascade changes), same SKUs:
| Case | Changed | Notes |
|---|---|---|
| `SHEET_UNDERCUT_WE_WIN` | 2 / 6 | Both were being **cut while holding the Buy Box with flat demand**: `UBMICROFIBERDUVETTWINWHITE` Decrease $17.06 โ **Increase $17.94** (`BEST_OBSERVED`), `UBMICROFIBERGUSSETPILLOWWHITEQUEEN` Decrease $25.01 โ **Increase $27.37**. The two corroborated SKUs were unchanged. |
| `AD_SPIRAL_LOW_STOCK` | 4 / 6 | `Investigate/AD_SPIRAL` โ `Increase/LOW_STOCK` |
| `AD_SPIRAL_NO_COST` | 6 / 6 | `Investigate/AD_SPIRAL` โ `Investigate/NO_COST_DATA` |
| **Targets below break-even, any arm** | **0** | |
**The invariant is measured on the TARGET, not the step-capped first move.** A SKU already
selling under its own ad-inclusive floor cannot be lifted over it in one 5% step, and
reporting that deliberate multi-cycle climb as a breach buries any real one. Two SKUs are
below floor and stepping up by design; both are reported separately.
---
## 7. Key formulas
| Quantity | Formula |
|---|---|
| Take-home / unit | `pยท(1 โ referral% โ returns%) โ landed โ FBA โ other` |
| Break-even | `(landed + FBA + returns + other) / (1 โ referral%)` |
| Elasticity | OLS on `ln(units/day) = a + eยทln(price)` over 6 months |
| Scenario demand | `units ร (p / pโ)^e` |
| Realization factor | `actual booked profit (window) รท modeled net at current price` |
| TACoS | `ad spend รท total revenue` (window) |
| Avg sold price | `revenue รท units` (window) โ reconciles the Current row |
| Best observed price | `avg_price` of the best-earning band โ **the price actually charged**, never the $0.50-rounded `price_band` key |
---
## 8. Repository map
```
pricing_agent/
โโโ app.py # dashboard (presentation only)
โโโ legacy_app.py # previous analyst UI (still runnable)
โโโ dashboard/
โ โโโ theme.py # CRAI design tokens + plotly template
โ โโโ live_data.py # COSMOS adapter, decision + scenario engine
โโโ src/pricing_agent/
โ โโโ analyze.py # per-SKU orchestration โ AnalysisResult
โ โโโ competitive_state.py # canonical Buy Box state + two-scraper reconciliation
โ โโโ elasticity.py # demand model + profit optimizer
โ โโโ performance.py # actual-profit evidence
โ โโโ tools/margin_engine.py # pure fee/break-even math (golden-tested)
โ โโโ cosmos/
โ โโโ client.py # auth + retry HTTP
โ โโโ service.py # endpoints, _exact_row, _flatten_takehome, INVP projections
โ โโโ models.py # typed COSMOS responses (pydantic)
โโโ config/ # settings + pricing_rules.yaml (incl. competitor thresholds)
โโโ scripts/
โ โโโ backtest_competitor_rules.py # verdict delta + old-vs-new policy delta
โโโ .streamlit/config.toml # CRAI theme
โโโ tests/ # margin-engine golden values, test_competitor_rules.py
# (fail-safe + ordering invariants), test_pricing_safety.py,
# test_cosmos.py (exact-SKU joins)
```
---
## 9. Known data gaps
- **Ad-attributed sales / ACoS / campaign budget** โ live in `/api/campaigns` + `/adsApi`,
not yet wired. Shown as "โ".
- **Historical competitor prices** โ Apify returns a current snapshot only.
- **`UBCFKFITTEDSHEETWHITECALKING` is not in COSMOS at all.** `sku=` returns zero rows on
any marketplace and the fee endpoint replies "Product not found". Likely delisted or
renamed. It now fails loudly (`get_product` โ `None`) rather than adopting another
product's identity. A catalogue question, not a code one.
---
## 10. Principles
1. **Deterministic core, narrative shell** โ every number is a formula over COSMOS data; language models only phrase explanations.
2. **Read-only** โ the agent proposes; a human approves; nothing writes back. `submit_price_approval` remains a stub with no callers.
3. **Honest gaps** โ missing upstream data shows "โ" or an explicit investigation, never a fabricated number. A wrong number is worse than a blank one.
4. **Facts vs projections are labeled** โ the Current row is real booked history; other prices are clearly modeled.
5. **Everything reconciles** โ one averaging window drives units, revenue, ads and profit so `price ร units = revenue` always holds.