Compare commits

..

34 Commits

Author SHA1 Message Date
codex
b7eedb90e1 Refresh scope document to match implemented core 2026-07-03 01:50:14 +02:00
codex
a9a55e19f1 Implement governance workflows and close WP-0008 2026-07-03 01:27:37 +02:00
codex
a76e57ba89 Implement Stripe publication layer and close WP-0007 2026-07-03 01:08:29 +02:00
codex
124ad48720 Implement customer-tuning solver and close WP-0006 2026-07-02 23:46:58 +02:00
codex
386c8a46fe Implement comparable LTV engine and close WP-0005 2026-07-02 22:50:16 +02:00
codex
656bbb81a5 Implement boundary engine and close WP-0004 2026-07-02 22:07:56 +02:00
codex
0a683aea5a Restore pytest execution and stabilize tests 2026-07-02 21:34:44 +02:00
codex
2a9a3e690f chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-02:
  - update .custodian-brief.md for adaptive-pricing
2026-07-02 21:33:10 +02:00
codex
1810ae6e29 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-02:
  - update .custodian-brief.md for adaptive-pricing
2026-07-02 21:32:21 +02:00
codex
6c6f3d40ae Implement canonical pricing core and close WP-0003 2026-07-02 20:48:16 +02:00
codex
ab700caa4b chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-02:
  - update .custodian-brief.md for adaptive-pricing
2026-07-02 20:46:54 +02:00
codex
75c94bbfe5 Add implementation roadmap and milestone workplans 2026-07-02 20:40:08 +02:00
codex
d11e7a0742 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-02:
  - update .custodian-brief.md for adaptive-pricing
2026-07-02 19:18:55 +02:00
codex
327d3c551b Normalize repo metadata and sync docs 2026-07-02 10:21:02 +02:00
0a38def5a5 Complete Economic Observatory MVP (ADAPTIVE-WP-0002)
Add file-based Bubble, Stripe, and OpenRouter importers; usage attribution,
cost allocation, pricing simulator, credit wallets, and recommendations in the
dashboard API. Document whynot-design UI workflow and archive the finished
workplan with all ten tasks marked done.
2026-06-22 23:23:31 +02:00
04ee6d2421 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-22:
  - update .custodian-brief.md for adaptive-pricing
2026-06-22 23:23:14 +02:00
4ef04dd5e5 observatory stuff 2026-06-22 23:05:05 +02:00
1bdb518a94 Human-review .repo-classification.yaml (CUST-WP-0050 follow-up) 2026-06-22 17:56:16 +02:00
f8bd6f912f Add .repo-classification.yaml (CUST-WP-0050 T11 agent first-pass) 2026-06-22 17:47:34 +02:00
a1a90a9504 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-22:
  - update .custodian-brief.md for adaptive-pricing
2026-06-22 17:05:08 +02:00
da3b7d66f0 Integrate whynot-design into Economic Observatory UI
Vendor whynot-design Layer 1 (tokens, CSS) and Layer 2 (<wn-*>
components) via scripts/sync-whynot-design.sh with a pinned ref.
Migrate the observatory shell to canonical web components, keep
observatory-specific layout in styles.css, and add vendor integrity
tests plus correct JS MIME types on the dev server.
2026-06-22 03:09:44 +02:00
9c1c2142fc Add Economic Observatory web UI with ledger-backed API
Introduce ui/ dashboard (dark observatory layout), JSON API, and local
dev server. All metrics load from expense and payment record ledgers.
Links Claude design reference for visual alignment.
2026-06-22 02:48:52 +02:00
7b84d34ea6 Record actual Stripe payment costs for tegwick membership
Update payment_records to 8.99 EUR gross, 0.44 EUR fees, 8.55 EUR net
payout to binky-hedgehog. Link member tegwick in membership ledger and
add Stripe reference catalog.
2026-06-22 02:36:42 +02:00
bb3f152846 Add railiance01 hosting at 8.99 EUR/mo from March 2026
Expense records and virtual server catalog entry for railiance01,
active from 2026-03 alongside existing domain and coulombcore costs.
2026-06-22 02:21:55 +02:00
fc2324692c Add coulombcore hosting expense at 13.99 EUR/mo from Jan 2025
One hosting expense record per month alongside domain rows. Update
infrastructure catalog, tests, and dashboard for 20.74 EUR/mo total
infrastructure.
2026-06-22 02:20:28 +02:00
86ce511764 Replace infrastructure costs with actual domain invoice data
Start ledger January 2025. Record coulomb.social (3.75 EUR/mo) and
coulomb.pro (3.00 EUR/mo) as per-domain expense rows. Remove Bubble and
placeholder overhead. Add infrastructure reference catalogs; virtual server
records pending invoice data.
2026-06-22 02:14:06 +02:00
31db9f8f31 Refactor economics to expense-record ledger with correct Bubble cost
Replace pre-aggregated costs.json with expense_records.json (48 line-item
records) and payment_records.json. All monthly and cumulative totals are
computed deterministically in observatory/ledger.py. Correct Bubble.io to
$32/mo (since Feb 2025) — infrastructure €69.44/mo not €72.20.
2026-06-22 02:03:22 +02:00
ea2c2c6403 Fix cumulative platform cost Stripe double-counting
Split infrastructure vs payment-processing costs. Liquidity burn now
uses infrastructure cash out only (€1,155.20 cumulative) because Stripe
fees are already deducted from net member payments. Total platform cost
(€1,158.24) remains visible for gross-margin economics.
2026-06-22 01:51:53 +02:00
fe2174f37a Add liquidity tracking, budget, and platform cost history
Restore operator platform costs (Bubble, domains, Stripe) with monthly
history from March 2025 and member payments from November 2025. Track
€1,000 starting budget, cumulative burn, and remaining liquidity in the
economics dashboard. Document LQ requirements in REQUIREMENTS.md.
2026-06-22 01:48:45 +02:00
8f42220d81 Adapt economics dashboard to sole-member zero-cost reality
Update membership, revenue, and cost registries to one active member with
no running costs. Refresh dashboard report and tests for €8.99 revenue and
100% gross margin.
2026-06-22 01:41:59 +02:00
a1a4aa972f Implement ADAPTIVE-WP-0002 Sprint 1 economic foundations
Add Coulomb observatory package with JSON registries (product, pricing
models, costs, revenue, membership), economics snapshot engine, Economics
Dashboard v1 CLI, sample report, and pytest coverage. Complete T01 and
queue Sprint 2 Bubble.io integration.
2026-06-22 01:32:48 +02:00
d648a3263d chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-22:
  - update .custodian-brief.md for adaptive-pricing
2026-06-22 01:32:42 +02:00
6c02a0cfa9 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-22:
  - update .custodian-brief.md for adaptive-pricing
2026-06-22 01:32:18 +02:00
594bb6c5f3 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-22:
  - ADAPTIVE-WP-0002-T02: todo → wait
2026-06-22 01:32:17 +02:00
123 changed files with 14401 additions and 177 deletions

20
.claude/rules/agents.md Normal file
View File

@@ -0,0 +1,20 @@
## Kaizen Agents
Specialized agent personas available on demand via the state-hub MCP.
**Discover:** `list_kaizen_agents()` — returns all agents with name, description, category
**Load:** `get_kaizen_agent("tdd-workflow")` — returns full instructions; read and follow them
Common agents:
| Agent | Category | When to use |
|-------|----------|-------------|
| `tdd-workflow` | testing | Step-by-step TDD8 workflow for any feature |
| `code-refactoring` | quality | Code quality analysis and safe refactoring |
| `test-maintenance` | testing | Diagnose and fix failing tests |
| `requirements-engineering` | process | Prevent interface/mock mismatches upfront |
| `keepaTodofile` | process | Maintain TODO.md during work |
| `project-management` | process | Track status, determine next steps |
| `datamodel-optimization` | quality | Optimize dataclasses and data structures |
All 17 agents: call `list_kaizen_agents()` for the full list.

View File

@@ -0,0 +1,38 @@
## Architecture
The repo has two layers:
1. Root framework layer
- `INTENT.md`, `docs/`, `research/`, and `registry/` define the generic
adaptive-pricing vocabulary, lifecycle model, and roadmap.
- `workplans/` is the repo-native source of truth for tracked work (ADR-001).
2. Project implementation layer
- `projects/coulomb-pricing/` contains the first concrete deployment:
Coulomb Social's Economic Observatory MVP.
- `observatory/` is a small Python package that reads JSON ledgers and
registries from `data/`, computes economics snapshots, and serves a local UI.
Current Coulomb data flow:
- `data/*.json` ledgers and registries
- `observatory/load.py` parses JSON into dataclasses
- `observatory/ledger.py` builds monthly cost rows
- `observatory/economics.py` computes liquidity, margins, and snapshots
- `observatory/allocation.py`, `usage.py`, `pricing_context.py`,
`simulator.py`, `credits.py`, and `recommendations.py` derive higher-level
pricing views
- `observatory/api.py` assembles the dashboard payload
- `observatory/__main__.py` renders the Markdown report
- `observatory/server.py` exposes `/api/dashboard` and serves `ui/`
External integrations are file-based in MVP:
- Bubble export importer
- Stripe export importer
- OpenRouter export importer
The internal model and ledgers are the source of truth. Provider exports feed
the ledgers; they do not replace them.
## Quick Reference
`~/state-hub/mcp_server/TOOLS.md` — MCP tool reference

View File

@@ -0,0 +1,38 @@
## First Session Protocol
Triggered when `get_domain_summary("financials")` shows **no workstreams**.
The project is registered but work has not yet been structured.
**Step 1 — Read, don't write**
- `~/the-custodian/canon/projects/financials/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/financials/roadmap_v0.1.md` — planned phases
- Scan repo root: README, directory structure, existing code or docs
**Step 2 — Survey in-progress work**
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
**Step 3 — Propose workstreams to Bernd**
Propose 13 workstreams — each a coherent strand, weeks to months, anchored to a
roadmap phase. **Wait for approval before creating.**
**Step 4 — Create workplan file first, then DB record (ADR-001)**
```
workplans/ADAPTIVE-WP-NNNN-<slug>.md ← write this first
```
Then register in the hub:
```
create_workstream(topic_id="f39fa2a3-c491-414c-a91b-b4c5fcc6139c", title="...", owner="...", description="...")
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
```
**Step 5 — Record the setup**
```
add_progress_event(
summary="First session: structured financials into N workstreams, M tasks",
event_type="milestone",
topic_id="f39fa2a3-c491-414c-a91b-b4c5fcc6139c",
detail={"workstreams": [...], "tasks_created": M}
)
```
<!-- Delete or archive this file once past first session -->

View File

@@ -0,0 +1,12 @@
## Repo boundary
This repo owns **adaptive-pricing** only. It does not own:
- State Hub server code, DB schema, or consistency tooling → `~/state-hub`
- SSH certificates, login flows, or secret vending → `ops-warden`,
`ops-bridge`, OpenBao, Keycloak, and related custody systems
- Bubble.io, Stripe, or OpenRouter product runtimes themselves; this repo only
models or imports their pricing-relevant data
- Generic whynot-design source assets; this repo only vendors the UI artifacts
needed by `projects/coulomb-pricing/ui/`
- Unrelated Coulomb or marketplace application code outside
`projects/coulomb-pricing/`

View File

@@ -0,0 +1,10 @@
**Purpose:** Auto-regulating market value exploring price engine.
**Domain:** financials
**Repo slug:** adaptive-pricing
**State Hub topic:** helix-forge
**Topic ID:** f39fa2a3-c491-414c-a91b-b4c5fcc6139c
Repo classification and hub topic are intentionally separate here:
- Repo/business domain: `financials`
- Shared hub topic: `helix-forge` in the hub's `infotech` domain

View File

@@ -0,0 +1,91 @@
## Session Protocol
Dev Hub (State Hub API): http://127.0.0.1:8000
MCP server name in `~/.claude.json`: `dev-hub`
**Step 1 — Orient**
Read the offline-safe brief first — it works without a live hub connection:
```bash
cat .custodian-brief.md
```
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
```
get_domain_summary("financials")
```
Repo-specific work tracking still syncs through topic
`f39fa2a3-c491-414c-a91b-b4c5fcc6139c` (`helix-forge`).
If MCP tools are unavailable in the current agent session, use the REST API:
```bash
curl -s "http://127.0.0.1:8000/state/summary" | python3 -m json.tool
```
If the hub is offline: `cd ~/state-hub && make api`
**Step 2 — Check inbox**
With MCP tools:
```
get_messages(to_agent="adaptive-pricing", unread_only=True)
```
Mark read with `mark_message_read(message_id)`. Reply or act on coordination
requests before proceeding.
Without MCP tools:
```bash
curl -s "http://127.0.0.1:8000/messages/?to_agent=adaptive-pricing&unread_only=true" \
| python3 -m json.tool
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
-H "Content-Type: application/json" -d '{}'
```
**Step 3 — Scan workplans**
```bash
ls workplans/
```
For each file with `status: ready`, `active`, or `blocked`, note pending
`wait`/`todo`/`progress` tasks.
**Step 4 — Present brief**
1. **Repo-relevant workstreams** under topic `helix-forge` — title, task counts, blocking decisions
2. **Pending tasks** from `workplans/` + any `[repo:adaptive-pricing]` hub tasks
3. **Goal guidance** — if `goal_guidance` in summary:
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
- `alignment_warnings`: flag if active work is not aligned with current goal
4. **Suggested next action** — highest-priority open item
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
If no workstreams: follow First Session Protocol (`first-session.md`).
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
**Session close:**
With MCP tools:
```
add_progress_event(summary="...", topic_id="f39fa2a3-c491-414c-a91b-b4c5fcc6139c", workstream_id="<uuid>")
```
Without MCP tools:
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{"topic_id":"f39fa2a3-c491-414c-a91b-b4c5fcc6139c","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
```
If workplan files were modified, ensure the local copy is up to date first:
```bash
git -C <repo_path> pull --ff-only
statehub fix-consistency
```
Fallback when the CLI is unavailable:
```bash
/home/worsch/state-hub/.venv/bin/python /home/worsch/state-hub/custodian_cli.py \
fix-consistency --repo adaptive-pricing --repo-path /home/worsch/adaptive-pricing
```
Legacy wrapper:
```bash
cd ~/state-hub && make fix-consistency REPO=adaptive-pricing
```
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
until you pull — intentional to prevent clobbering remote progress.

View File

@@ -0,0 +1,28 @@
## Stack
- **Language:** Markdown-heavy repo with a Python 3 implementation subtree
- **Key deps:** Python stdlib for the observatory runtime, `pytest` for tests,
optional `make` for wrappers, whynot-design vendored UI assets in
`projects/coulomb-pricing/ui/vendor/`
## Dev Commands
```bash
# Root repo: metadata/workplan sync
statehub fix-consistency
/home/worsch/state-hub/.venv/bin/python /home/worsch/state-hub/custodian_cli.py \
fix-consistency --repo adaptive-pricing --repo-path /home/worsch/adaptive-pricing
# Project runtime
cd /home/worsch/adaptive-pricing/projects/coulomb-pricing
python3 -m observatory --period 2026-06
python3 -m observatory --period 2026-06 --output reports/economics-2026-06.md
python3 -m observatory.server
# Tests
python3 -m pytest -q
make test
# UI vendor refresh
make design
```

View File

@@ -0,0 +1,40 @@
## Workplan Convention (ADR-001)
File location: `workplans/ADAPTIVE-WP-NNNN-<slug>.md`
ID prefix: `ADAPTIVE-WP-`
Work items originate as files in this repo **before** being registered in the hub.
Canonical workplan/workstream frontmatter statuses are:
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
Use `proposed` for a newly drafted plan, `ready` after review against current
repo state, and `finished` when implementation is complete. `stalled` and
`needs_review` are derived health labels, not stored statuses.
Closed workplans may be moved to `workplans/archived/` with a completion-date
prefix: `YYMMDD-ADAPTIVE-WP-NNNN-<slug>.md`. The frontmatter id remains
unchanged; the prefix is only for quick visual reference.
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
directly. Promote anything requiring analysis, design, approval, dependencies, or
multiple planned phases into a normal workplan.
Ecosystem todos from other agents arrive as `[repo:adaptive-pricing]` hub tasks —
visible at session start. Pick one up by creating the workplan file, then registering
the workstream.
Task blocks use this shape:
```task
id: ADAPTIVE-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
```
Status progression is `todo``progress``done`; use `wait` for waiting or
blocked work and `cancel` for stopped work.
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->

View File

@@ -1,29 +1,18 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — adaptive-pricing
**Domain:** helix_forge
**Last synced:** 2026-06-21 23:19 UTC
**Domain:** financials
**Last synced:** 2026-07-02 19:33 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Economic Observatory MVP (Coulomb Social)
Progress: 0/8 done | workstream_id: `9e0b7784-702a-4bc7-b7a1-3ff801f9c768`
**Open tasks:**
- ! Sprint 2 — Bubble.io Integration `42c181f9`
- ! Sprint 3 — Stripe Integration `c7e308bc`
- ! Sprint 4 — OpenRouter Cost Attribution `b2b61910`
- ! Sprint 5 — Cost Allocation Engine `906009be`
- ! Sprint 6 — Pricing Simulator `cb735e7d`
- ! Sprint 7 — Membership Credit System `aa8efb52`
- ! Sprint 8 — Adaptive Pricing Prototype `d8195bf0`
- … and 1 more open tasks
*(none — repo may need first-session setup)*
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("helix_forge")`
`get_domain_summary("financials")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

27
.repo-classification.yaml Normal file
View File

@@ -0,0 +1,27 @@
repo_classification:
standard: Repo Classification Standard
version: '1.0'
classified_at: '2026-06-22'
classified_by: human
category: product
domain: financials
secondary_domains:
- infotech
- agents
capability_tags:
- pricing
- monetization
- lifecycle
- decision-support
- product-development
business_stake:
- finance
- product
- sales
- intelligence
- automation
business_mechanics:
- intention
- control
- adaptation
notes: Adaptive pricing product; standard §13.6 — human confirmed.

View File

@@ -4,33 +4,16 @@
**Purpose:** Auto-regulating market value exploring price engine.
**Domain:** helix_forge
**Primary repo domain:** financials
**Repo slug:** adaptive-pricing
**State Hub topic:** `helix-forge`
**Topic ID:** `f39fa2a3-c491-414c-a91b-b4c5fcc6139c`
**Workplan prefix:** `ADAPTIVE-WP-`
---
## Dev Workflow
The repository is in an **early framework phase**: Markdown documentation, research
notes, and capability registry YAML. No application runtime, package manifest, or
automated test suite exists yet. Executable implementation begins under
`workplans/ADAPTIVE-WP-0002-economic-observatory-mvp.md`.
| Need | Command |
|------|---------|
| Install | none — no runtime dependencies |
| Test | none configured yet |
| Lint / format | none configured — match surrounding Markdown style |
| Build | none — documentation-only repo |
| Run | none |
| Workplan / hub sync | `cd ~/state-hub && make fix-consistency REPO=adaptive-pricing REPO_PATH=~/adaptive-pricing` |
| Registry sanity | `grep -q '^version:' registry/indexes/capabilities.yaml && echo OK` |
**Verify a change before declaring it done:** run `make fix-consistency` (expect
PASS), and confirm edited docs stay aligned with `INTENT.md` and
`docs/ProductRequirementsDocument.md`.
`adaptive-pricing` is classified as a `financials` repo in
`.repo-classification.yaml`, but State Hub coordination currently runs through
the shared `helix-forge` topic in the hub's `infotech` domain. Keep repo-domain
fields (`domain`) and hub-topic fields (`topic_slug`, `topic_id`) distinct.
---
@@ -50,7 +33,7 @@ there is no MCP server for Codex agents.
# Offline brief — works without hub connection
cat .custodian-brief.md
# Active workstreams for this domain
# Active workstreams for this repo's hub topic
curl -s "http://127.0.0.1:8000/workstreams/?topic_id=f39fa2a3-c491-414c-a91b-b4c5fcc6139c&status=active" \
| python3 -m json.tool
@@ -103,7 +86,7 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
## Session Protocol
**Start:**
1. `cat .custodian-brief.md`domain goal and open workstreams (offline-safe)
1. `cat .custodian-brief.md`hub-topic goal and open workstreams (offline-safe)
2. Check inbox: `GET /messages/?to_agent=adaptive-pricing&unread_only=true`; mark read
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
@@ -115,12 +98,22 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
**Close:**
1. Update workplan file task statuses to reflect progress
2. Log: `POST /progress/` with a summary of what changed
3. Note for the custodian operator: after workplan file changes, run from
`~/state-hub`:
3. After workplan file changes, run:
```bash
statehub fix-consistency
```
Fallback when the CLI is unavailable:
```bash
/home/worsch/state-hub/.venv/bin/python /home/worsch/state-hub/custodian_cli.py \
fix-consistency --repo adaptive-pricing --repo-path /home/worsch/adaptive-pricing
```
Legacy wrapper:
```bash
cd ~/state-hub
make fix-consistency REPO=adaptive-pricing
```
This syncs task status from files into the hub DB.
Coding agents should run the direct CLI when available. This syncs task
status from files into the hub DB.
---
@@ -175,8 +168,6 @@ get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
---
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
@@ -186,12 +177,42 @@ get wrong.
| Path | Purpose |
|------|---------|
| `INTENT.md`, `docs/`, `research/`, `registry/` | Generic adaptive-pricing framework |
| `projects/<slug>/` | Deployment-specific MVP material (integrations, configs, project docs) |
| `projects/<slug>/` | Deployment-specific implementations, integrations, data, and project docs |
| `workplans/` | ADR-001 workplans and task tracking (including MVP execution plans) |
Do not place project-specific MVP documentation in `specs/` or other generic
paths. The Coulomb Social MVP lives under `projects/coulomb-pricing/`; its
workplan remains `workplans/ADAPTIVE-WP-0002-economic-observatory-mvp.md`.
Coulomb MVP workplan is archived at
`workplans/archived/260622-ADAPTIVE-WP-0002-economic-observatory-mvp.md`.
## Developer Workflow
The root repo is documentation- and workplan-heavy. The current executable
runtime lives under `projects/coulomb-pricing/`.
```bash
# Generate the Coulomb observatory Markdown report
cd projects/coulomb-pricing
python3 -m observatory --period 2026-06
# Start the local observatory UI
python3 -m observatory.server
# Run tests when pytest is available
python3 -m pytest -q
# Makefile wrappers when make is available
make test
make serve
make design
# Sync workplan metadata back into the hub after workplan edits
statehub fix-consistency
# Fallback when the CLI is not installed
/home/worsch/state-hub/.venv/bin/python /home/worsch/state-hub/custodian_cli.py \
fix-consistency --repo adaptive-pricing --repo-path /home/worsch/adaptive-pricing
```
---
@@ -218,11 +239,11 @@ anything needing analysis, design, approval, dependencies, or multiple phases.
id: ADAPTIVE-WP-NNNN
type: workplan
title: "..."
domain: helix_forge
domain: financials
repo: adaptive-pricing
status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex
topic_slug: ...
topic_slug: helix-forge
created: "YYYY-MM-DD"
updated: "YYYY-MM-DD"
state_hub_workstream_id: "<uuid>" # written by fix-consistency — do not edit
@@ -252,5 +273,6 @@ Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blo
To create a new workplan:
1. Write the file following the format above
2. Notify the custodian operator to run `make fix-consistency REPO=adaptive-pricing`
(or send a message to the hub agent via `POST /messages/`)
2. Run `statehub fix-consistency` locally; use the direct CLI fallback above if
`statehub` is not installed. Ask the operator only if the CLI or State Hub
API is unavailable.

12
CLAUDE.md Normal file
View File

@@ -0,0 +1,12 @@
# adaptive-pricing — Claude Code Instructions
@SCOPE.md
@.claude/rules/repo-identity.md
@.claude/rules/session-protocol.md
@.claude/rules/first-session.md
@.claude/rules/workplan-convention.md
@.claude/rules/stack-and-commands.md
@.claude/rules/architecture.md
@.claude/rules/repo-boundary.md
@.claude/rules/credential-routing.md
@.claude/rules/agents.md

View File

@@ -12,12 +12,16 @@ pricing to payment-provider execution.
|-----|---------|
| [INTENT.md](INTENT.md) | Project purpose, problem space, lifecycle model |
| [docs/ProductRequirementsDocument.md](docs/ProductRequirementsDocument.md) | Generic product requirements |
| [docs/ImplementationRoadmap.md](docs/ImplementationRoadmap.md) | Milestone-based implementation path from observatory MVP to adaptive engine |
| [docs/StripePublication.md](docs/StripePublication.md) | Provider-neutral publication model and Stripe shadow-publication flow |
| [docs/GovernanceWorkflows.md](docs/GovernanceWorkflows.md) | Governance policy, recommendation workflow, tuning contract, and audit surfaces |
| [AGENTS.md](AGENTS.md) | Agent instructions, dev workflow, State Hub integration |
| [workplans/](workplans/) | Active workstreams and tasks |
| [projects/coulomb-pricing/](projects/coulomb-pricing/) | Coulomb Social MVP deployment material |
## Status
Early framework phase (documentation and research). First implementation:
[Economic Observatory MVP](workplans/ADAPTIVE-WP-0002-economic-observatory-mvp.md)
for Coulomb Social.
Framework-first repo with one finished project-specific implementation:
[Economic Observatory MVP](workplans/archived/260622-ADAPTIVE-WP-0002-economic-observatory-mvp.md)
for Coulomb Social. The root remains the generic pricing framework; the current
runtime lives under `projects/coulomb-pricing/observatory/`.

View File

@@ -12,30 +12,64 @@ adapting, and implementing pricing models across the product lifecycle. See
## In Scope
- Generic framework documentation (`INTENT.md`, `docs/`, `research/`, `registry/`).
- Pricing model vocabulary, lifecycle reasoning, and capability registry.
- Project-specific deployments under `projects/<slug>/`.
- Generic pricing core under `adaptive_pricing_core/`, including:
canonical pricing schema and validation, boundary evaluation, comparable
customer LTV, customer-tuning solver, provider-publication primitives, Stripe
mapping, and governance models.
- Generic framework documentation and research (`INTENT.md`, `docs/`,
`research/`, `registry/`).
- Project-specific proving grounds under `projects/<slug>/`, with Coulomb
Social as the first full adopter of the generic core.
- Local execution surfaces that keep internal pricing definitions as the source
of truth, including provider shadow publication, governed recommendations,
tuning contracts, health checks, and audit history.
- State Hub workplans under `workplans/` (ADR-001).
## Out of Scope
- Owning unrelated adjacent systems (Bubble.io, Stripe, OpenRouter runtimes).
- Making irreversible operational or pricing decisions without human approval.
- Project-specific MVP material in generic doc paths (use `projects/<slug>/`).
- Owning unrelated adjacent systems as the source of truth, including Bubble,
Stripe, and OpenRouter runtimes.
- Live payment-provider mutation as the default repo behavior. Current provider
execution stops at mapping, shadow-state publication, drift detection, and
rollback planning.
- Treating approximate provider mappings as fully enforced billing behavior when
they still require supplemental operational or contract logic.
- Autonomous customer-visible pricing rollouts or irreversible pricing changes
without human approval.
- Project-specific deployment material in generic doc paths (use
`projects/<slug>/`).
## Current State
- **Phase:** early framework — documentation, research, and registry scaffolding.
- **Runtime:** none in this repo yet; first implementation is the Coulomb Social
Economic Observatory MVP (`ADAPTIVE-WP-0002`).
- **Bootstrap:** State Hub integration (`ADAPTIVE-WP-0001`) wires agent orientation,
workplan tracking, and custodian brief sync.
- **Phase:** framework plus executable core. The repo is no longer just docs and
research around a single MVP; it now contains reusable pricing-core modules
and one concrete deployment.
- **Implemented milestones:** `ADAPTIVE-WP-0003` through `ADAPTIVE-WP-0008`
are finished. The repo now has canonical pricing models, explainable boundary
validation, comparable-customer LTV simulation, customer-tuning, provider
shadow publication, and governance workflows.
- **Generic runtime surface:** reusable implementation lives under
`adaptive_pricing_core/`.
- **Deployment surface:** Coulomb Social remains the first proving ground under
`projects/coulomb-pricing/observatory/`, including the dashboard/API, data
loaders, tuning pilot, Stripe shadow-publication flow, and governed
recommendation surfaces.
- **Execution boundary:** Stripe support is currently a provider abstraction and
local shadow-state publisher, not live Stripe API management.
- **Coordination:** State Hub integration (`ADAPTIVE-WP-0001`) remains the repo
workflow backbone for orientation, workplan tracking, and brief sync.
## Getting Oriented
- Start with: `INTENT.md`
- Product requirements (generic): `docs/ProductRequirementsDocument.md`
- Canonical pricing schema: `docs/PricingModelSchema.md`
- Customer tuning: `docs/CustomerTuningSolver.md`
- Provider publication: `docs/StripePublication.md`
- Governance workflow: `docs/GovernanceWorkflows.md`
- Implementation roadmap: `docs/ImplementationRoadmap.md`
- Agent instructions: `AGENTS.md`
- Workplans: `workplans/`
- Coulomb MVP artifacts: `projects/coulomb-pricing/`
- Generic code: `adaptive_pricing_core/`
- Coulomb deployment artifacts: `projects/coulomb-pricing/`
- Offline hub brief: `.custodian-brief.md`

View File

@@ -0,0 +1,61 @@
from .boundary_engine import (
BoundaryPolicy,
CommitmentTerms,
ConstraintResult,
PricingConfiguration,
ValidationResult,
default_commitment_terms,
validate_pricing_configuration,
)
from .comparable_ltv import (
ComparableCustomerProfile,
ComparableLTVEstimate,
LTVPolicy,
PricingComparison,
PricingModelComparison,
SensitivityCase,
SensitivityOutcome,
compare_pricing_configurations,
default_sensitivity_cases,
estimate_comparable_customer_ltv,
select_reference_estimate,
)
from .pricing_models import (
ChargeComponent,
Commitment,
PricingModel,
PricingModelStatus,
TunableParameter,
load_pricing_models,
validate_pricing_catalog,
validate_pricing_model,
)
__all__ = [
"BoundaryPolicy",
"ChargeComponent",
"ComparableCustomerProfile",
"ComparableLTVEstimate",
"Commitment",
"CommitmentTerms",
"ConstraintResult",
"LTVPolicy",
"PricingModel",
"PricingComparison",
"PricingModelComparison",
"PricingModelStatus",
"PricingConfiguration",
"SensitivityCase",
"SensitivityOutcome",
"TunableParameter",
"ValidationResult",
"compare_pricing_configurations",
"default_sensitivity_cases",
"default_commitment_terms",
"estimate_comparable_customer_ltv",
"load_pricing_models",
"select_reference_estimate",
"validate_pricing_configuration",
"validate_pricing_catalog",
"validate_pricing_model",
]

View File

@@ -0,0 +1,937 @@
from __future__ import annotations
from dataclasses import dataclass, field
from decimal import Decimal, ROUND_HALF_UP
from typing import Any, Callable, Literal
from .pricing_models import PricingModel
ConstraintSeverity = Literal["hard", "soft"]
ConstraintStatus = Literal["pass", "fail", "review"]
ValidationDecision = Literal["accepted", "requires_approval", "rejected"]
ConstraintEvaluator = Callable[
["PricingConfiguration", "BoundaryPolicy", "PricingMetrics", "PricingMetrics"],
"ConstraintResult",
]
TWOPLACES = Decimal("0.01")
PCTPLACES = Decimal("0.1")
def _money(value: Decimal) -> Decimal:
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
def _percent(value: Decimal) -> Decimal:
return value.quantize(PCTPLACES, rounding=ROUND_HALF_UP)
def _decimal(value: Decimal | str | int | float | None) -> Decimal:
if value in (None, ""):
return Decimal("0")
return Decimal(str(value))
def _recurring_non_usage_component_revenue(model: PricingModel) -> Decimal:
total = Decimal("0")
for component in model.charge_components:
if component.kind in {"access", "usage", "setup"}:
continue
if component.amount is None:
continue
if component.cadence == "one_time":
continue
total += component.amount
return total
def _access_fee_amount(model: PricingModel) -> Decimal:
for component in model.charge_components:
if component.kind == "access" and component.amount is not None:
return component.amount
return model.access_fee_amount
def _usage_component(model: PricingModel):
return next((component for component in model.charge_components if component.kind == "usage"), None)
def _tunable_default(model: PricingModel, key: str) -> Decimal | None:
parameter = next((item for item in model.tunable_parameters if item.key == key), None)
if not parameter or parameter.default_value in (None, ""):
return None
return Decimal(str(parameter.default_value))
def _default_included_units(model: PricingModel) -> Decimal:
usage = _usage_component(model)
if usage and usage.included_units is not None:
return usage.included_units
value = _tunable_default(model, "included_tokens")
return value if value is not None else Decimal("0")
def _default_usage_unit_price(model: PricingModel) -> Decimal:
usage = _usage_component(model)
if usage and usage.unit_price is not None:
return usage.unit_price
value = _tunable_default(model, "overage_unit_price")
return value if value is not None else Decimal("0")
def _months_from_commitment(value: Decimal, unit: str | None) -> int:
normalized = (unit or "month").lower()
if normalized in {"month", "months"}:
return int(value)
if normalized in {"year", "years"}:
return int(value * Decimal("12"))
return int(value)
@dataclass(frozen=True)
class CommitmentTerms:
contract_duration_months: int | None = None
minimum_monthly_turnover: Decimal = Decimal("0")
prepaid_amount: Decimal = Decimal("0")
guaranteed_platform_fee: Decimal = Decimal("0")
customer_funded_onboarding: Decimal = Decimal("0")
reduced_cancellation_flexibility: bool = False
def default_commitment_terms(model: PricingModel) -> CommitmentTerms:
contract_duration = None
minimum_turnover = Decimal("0")
prepaid_amount = Decimal("0")
guaranteed_platform_fee = Decimal("0")
for commitment in model.commitments:
raw_value = _decimal(commitment.value)
if commitment.kind == "contract_duration":
contract_duration = _months_from_commitment(raw_value, commitment.unit)
elif commitment.kind in {"minimum_turnover", "minimum_monthly_turnover"}:
minimum_turnover = raw_value
elif commitment.kind in {"guaranteed_platform_fee", "minimum_platform_fee"}:
guaranteed_platform_fee = raw_value
elif commitment.kind == "prepayment" and (commitment.unit or "").lower() in {"eur", "usd"}:
prepaid_amount = raw_value
tunable_contract_duration = _tunable_default(model, "contract_duration_months")
if contract_duration is None and tunable_contract_duration is not None:
contract_duration = int(tunable_contract_duration)
return CommitmentTerms(
contract_duration_months=contract_duration,
minimum_monthly_turnover=minimum_turnover,
prepaid_amount=prepaid_amount,
guaranteed_platform_fee=guaranteed_platform_fee,
)
@dataclass(frozen=True)
class PricingConfiguration:
model: PricingModel
segment: str | None = None
expected_usage_units: Decimal = Decimal("0")
expected_usage_variance_pct: Decimal = Decimal("0")
allocated_fixed_cost: Decimal = Decimal("0")
direct_cost_amount: Decimal = Decimal("0")
unit_cost: Decimal = Decimal("0")
support_cost: Decimal = Decimal("0")
onboarding_cost: Decimal = Decimal("0")
risk_cost: Decimal = Decimal("0")
payment_fee_fixed: Decimal = Decimal("0")
payment_fee_rate_pct: Decimal = Decimal("0")
access_fee_amount: Decimal | None = None
included_units: Decimal | None = None
usage_unit_price: Decimal | None = None
discount_amount: Decimal = Decimal("0")
commitment_terms: CommitmentTerms = field(default_factory=CommitmentTerms)
@dataclass(frozen=True)
class BoundaryPolicy:
minimum_margin_pct: Decimal = Decimal("0")
target_margin_pct: Decimal = Decimal("15")
max_payment_fee_pct: Decimal = Decimal("10")
max_expected_usage_variance_pct: Decimal = Decimal("50")
approval_discount_pct: Decimal = Decimal("10")
max_discount_pct: Decimal = Decimal("25")
minimum_contract_duration_for_discount_months: int = 3
minimum_turnover_multiple_for_discount: Decimal = Decimal("1")
minimum_prepayment_months_for_discount: Decimal = Decimal("1")
@dataclass(frozen=True)
class PricingMetrics:
currency: str
monthly_revenue: Decimal
effective_monthly_revenue: Decimal
payment_fees: Decimal
payment_fee_pct: Decimal
allocated_fixed_cost: Decimal
direct_cost_amount: Decimal
variable_usage_cost: Decimal
support_cost: Decimal
onboarding_cost: Decimal
customer_funded_onboarding: Decimal
risk_cost: Decimal
total_monthly_cost: Decimal
monthly_margin: Decimal
margin_pct: Decimal
cost_floor_revenue: Decimal
minimum_margin_revenue: Decimal
target_margin_revenue: Decimal
expected_usage_units: Decimal
included_units: Decimal
billable_usage_units: Decimal
unit_cost: Decimal
usage_unit_price: Decimal
access_fee_amount: Decimal
contract_duration_months: int
minimum_monthly_turnover: Decimal
prepaid_amount: Decimal
guaranteed_platform_fee: Decimal
concession_value: Decimal
concession_pct: Decimal
baseline_monthly_revenue: Decimal
baseline_margin: Decimal
baseline_margin_pct: Decimal
meaningful_commitment_signals: tuple[str, ...]
reduced_cancellation_flexibility: bool
@dataclass(frozen=True)
class ConstraintResult:
id: str
title: str
severity: ConstraintSeverity
status: ConstraintStatus
summary: str
reason: str
actual_value: Decimal | str | int | None = None
threshold_value: Decimal | str | int | None = None
unit: str | None = None
details: dict[str, Any] = field(default_factory=dict)
suggested_action: str | None = None
@dataclass(frozen=True)
class BoundaryConstraint:
id: str
title: str
severity: ConstraintSeverity
evaluator: ConstraintEvaluator
@dataclass(frozen=True)
class ValidationResult:
model_id: str
model_name: str
decision: ValidationDecision
valid: bool
requires_approval: bool
summary: str
configuration: dict[str, Any]
metrics: PricingMetrics
policy: BoundaryPolicy
constraints: tuple[ConstraintResult, ...]
def _required_revenue(cost: Decimal, margin_pct: Decimal) -> Decimal:
if margin_pct >= Decimal("100"):
return Decimal("Infinity")
ratio = Decimal("1") - (margin_pct / Decimal("100"))
if ratio <= Decimal("0"):
return Decimal("Infinity")
return _money(cost / ratio)
def _meaningful_commitment_signals(
metrics: PricingMetrics,
baseline_metrics: PricingMetrics,
policy: BoundaryPolicy,
) -> tuple[str, ...]:
signals: list[str] = []
if metrics.minimum_monthly_turnover >= (
metrics.monthly_revenue * policy.minimum_turnover_multiple_for_discount
) and metrics.minimum_monthly_turnover > Decimal("0"):
signals.append("minimum_monthly_turnover")
if metrics.prepaid_amount >= (
metrics.monthly_revenue * policy.minimum_prepayment_months_for_discount
) and metrics.prepaid_amount > Decimal("0"):
signals.append("prepayment")
if metrics.guaranteed_platform_fee >= metrics.monthly_revenue and metrics.guaranteed_platform_fee > Decimal("0"):
signals.append("guaranteed_platform_fee")
if metrics.customer_funded_onboarding >= metrics.onboarding_cost and metrics.onboarding_cost > Decimal("0"):
signals.append("customer_funded_onboarding")
if (
metrics.contract_duration_months >= policy.minimum_contract_duration_for_discount_months
and metrics.contract_duration_months > baseline_metrics.contract_duration_months
):
signals.append("longer_contract_duration")
if metrics.reduced_cancellation_flexibility:
signals.append("reduced_cancellation_flexibility")
return tuple(signals)
def _build_metrics(
configuration: PricingConfiguration,
policy: BoundaryPolicy,
*,
baseline_metrics: PricingMetrics | None = None,
) -> PricingMetrics:
model = configuration.model
defaults = default_commitment_terms(model)
access_fee_amount = configuration.access_fee_amount
if access_fee_amount is None:
access_fee_amount = _access_fee_amount(model)
included_units = configuration.included_units
if included_units is None:
included_units = _default_included_units(model)
usage_unit_price = configuration.usage_unit_price
if usage_unit_price is None:
usage_unit_price = _default_usage_unit_price(model)
contract_duration_months = configuration.commitment_terms.contract_duration_months
if contract_duration_months is None:
contract_duration_months = defaults.contract_duration_months or 1
minimum_monthly_turnover = (
configuration.commitment_terms.minimum_monthly_turnover
if configuration.commitment_terms.minimum_monthly_turnover > Decimal("0")
else defaults.minimum_monthly_turnover
)
prepaid_amount = (
configuration.commitment_terms.prepaid_amount
if configuration.commitment_terms.prepaid_amount > Decimal("0")
else defaults.prepaid_amount
)
guaranteed_platform_fee = (
configuration.commitment_terms.guaranteed_platform_fee
if configuration.commitment_terms.guaranteed_platform_fee > Decimal("0")
else defaults.guaranteed_platform_fee
)
customer_funded_onboarding = configuration.commitment_terms.customer_funded_onboarding
reduced_cancellation_flexibility = configuration.commitment_terms.reduced_cancellation_flexibility
expected_usage_units = _decimal(configuration.expected_usage_units)
billable_usage_units = max(expected_usage_units - included_units, Decimal("0"))
recurring_revenue = (
access_fee_amount
+ _recurring_non_usage_component_revenue(model)
+ (usage_unit_price * billable_usage_units)
- configuration.discount_amount
)
monthly_revenue = _money(max(recurring_revenue, Decimal("0")))
effective_monthly_revenue = _money(
max(monthly_revenue, minimum_monthly_turnover, guaranteed_platform_fee)
)
payment_fees = _money(
configuration.payment_fee_fixed
+ (effective_monthly_revenue * configuration.payment_fee_rate_pct / Decimal("100"))
)
if effective_monthly_revenue > Decimal("0"):
payment_fee_pct = _percent(
(payment_fees / effective_monthly_revenue) * Decimal("100")
)
else:
payment_fee_pct = Decimal("100.0") if payment_fees > Decimal("0") else Decimal("0.0")
variable_usage_cost = _money(expected_usage_units * configuration.unit_cost)
residual_onboarding_cost = max(
configuration.onboarding_cost - customer_funded_onboarding,
Decimal("0"),
)
total_monthly_cost = _money(
configuration.allocated_fixed_cost
+ configuration.direct_cost_amount
+ variable_usage_cost
+ configuration.support_cost
+ residual_onboarding_cost
+ configuration.risk_cost
+ payment_fees
)
monthly_margin = _money(effective_monthly_revenue - total_monthly_cost)
if effective_monthly_revenue > Decimal("0"):
margin_pct = _percent((monthly_margin / effective_monthly_revenue) * Decimal("100"))
else:
margin_pct = Decimal("-100.0") if total_monthly_cost > Decimal("0") else Decimal("0.0")
cost_floor_revenue = _money(total_monthly_cost)
minimum_margin_revenue = _required_revenue(total_monthly_cost, policy.minimum_margin_pct)
target_margin_revenue = _required_revenue(total_monthly_cost, policy.target_margin_pct)
baseline_revenue = Decimal("0")
baseline_margin = Decimal("0")
baseline_margin_pct = Decimal("0.0")
concession_value = Decimal("0")
concession_pct = Decimal("0.0")
meaningful_commitment_signals: tuple[str, ...] = ()
baseline_contract_duration = contract_duration_months
if baseline_metrics is not None:
baseline_revenue = baseline_metrics.effective_monthly_revenue
baseline_margin = baseline_metrics.monthly_margin
baseline_margin_pct = baseline_metrics.margin_pct
concession_value = _money(max(baseline_metrics.monthly_margin - monthly_margin, Decimal("0")))
if baseline_metrics.effective_monthly_revenue > Decimal("0"):
concession_pct = _percent(
(concession_value / baseline_metrics.effective_monthly_revenue) * Decimal("100")
)
else:
concession_pct = Decimal("0.0")
baseline_contract_duration = baseline_metrics.contract_duration_months
probe_baseline = baseline_metrics
if probe_baseline is None:
probe_baseline = PricingMetrics(
currency=model.currency,
monthly_revenue=monthly_revenue,
effective_monthly_revenue=effective_monthly_revenue,
payment_fees=payment_fees,
payment_fee_pct=payment_fee_pct,
allocated_fixed_cost=_money(configuration.allocated_fixed_cost),
direct_cost_amount=_money(configuration.direct_cost_amount),
variable_usage_cost=variable_usage_cost,
support_cost=_money(configuration.support_cost),
onboarding_cost=_money(configuration.onboarding_cost),
customer_funded_onboarding=_money(customer_funded_onboarding),
risk_cost=_money(configuration.risk_cost),
total_monthly_cost=total_monthly_cost,
monthly_margin=monthly_margin,
margin_pct=margin_pct,
cost_floor_revenue=cost_floor_revenue,
minimum_margin_revenue=minimum_margin_revenue,
target_margin_revenue=target_margin_revenue,
expected_usage_units=expected_usage_units,
included_units=included_units,
billable_usage_units=billable_usage_units,
unit_cost=configuration.unit_cost,
usage_unit_price=usage_unit_price,
access_fee_amount=access_fee_amount,
contract_duration_months=baseline_contract_duration,
minimum_monthly_turnover=minimum_monthly_turnover,
prepaid_amount=prepaid_amount,
guaranteed_platform_fee=guaranteed_platform_fee,
concession_value=Decimal("0"),
concession_pct=Decimal("0.0"),
baseline_monthly_revenue=effective_monthly_revenue,
baseline_margin=monthly_margin,
baseline_margin_pct=margin_pct,
meaningful_commitment_signals=(),
reduced_cancellation_flexibility=reduced_cancellation_flexibility,
)
meaningful_commitment_signals = _meaningful_commitment_signals(
PricingMetrics(
currency=model.currency,
monthly_revenue=monthly_revenue,
effective_monthly_revenue=effective_monthly_revenue,
payment_fees=payment_fees,
payment_fee_pct=payment_fee_pct,
allocated_fixed_cost=_money(configuration.allocated_fixed_cost),
direct_cost_amount=_money(configuration.direct_cost_amount),
variable_usage_cost=variable_usage_cost,
support_cost=_money(configuration.support_cost),
onboarding_cost=_money(configuration.onboarding_cost),
customer_funded_onboarding=_money(customer_funded_onboarding),
risk_cost=_money(configuration.risk_cost),
total_monthly_cost=total_monthly_cost,
monthly_margin=monthly_margin,
margin_pct=margin_pct,
cost_floor_revenue=cost_floor_revenue,
minimum_margin_revenue=minimum_margin_revenue,
target_margin_revenue=target_margin_revenue,
expected_usage_units=expected_usage_units,
included_units=included_units,
billable_usage_units=billable_usage_units,
unit_cost=configuration.unit_cost,
usage_unit_price=usage_unit_price,
access_fee_amount=access_fee_amount,
contract_duration_months=contract_duration_months,
minimum_monthly_turnover=minimum_monthly_turnover,
prepaid_amount=prepaid_amount,
guaranteed_platform_fee=guaranteed_platform_fee,
concession_value=concession_value,
concession_pct=concession_pct,
baseline_monthly_revenue=baseline_revenue,
baseline_margin=baseline_margin,
baseline_margin_pct=baseline_margin_pct,
meaningful_commitment_signals=(),
reduced_cancellation_flexibility=reduced_cancellation_flexibility,
),
probe_baseline,
policy,
)
return PricingMetrics(
currency=model.currency,
monthly_revenue=monthly_revenue,
effective_monthly_revenue=effective_monthly_revenue,
payment_fees=payment_fees,
payment_fee_pct=payment_fee_pct,
allocated_fixed_cost=_money(configuration.allocated_fixed_cost),
direct_cost_amount=_money(configuration.direct_cost_amount),
variable_usage_cost=variable_usage_cost,
support_cost=_money(configuration.support_cost),
onboarding_cost=_money(configuration.onboarding_cost),
customer_funded_onboarding=_money(customer_funded_onboarding),
risk_cost=_money(configuration.risk_cost),
total_monthly_cost=total_monthly_cost,
monthly_margin=monthly_margin,
margin_pct=margin_pct,
cost_floor_revenue=cost_floor_revenue,
minimum_margin_revenue=minimum_margin_revenue,
target_margin_revenue=target_margin_revenue,
expected_usage_units=expected_usage_units,
included_units=included_units,
billable_usage_units=billable_usage_units,
unit_cost=configuration.unit_cost,
usage_unit_price=usage_unit_price,
access_fee_amount=access_fee_amount,
contract_duration_months=contract_duration_months,
minimum_monthly_turnover=minimum_monthly_turnover,
prepaid_amount=prepaid_amount,
guaranteed_platform_fee=guaranteed_platform_fee,
concession_value=concession_value,
concession_pct=concession_pct,
baseline_monthly_revenue=_money(baseline_revenue),
baseline_margin=_money(baseline_margin),
baseline_margin_pct=baseline_margin_pct,
meaningful_commitment_signals=meaningful_commitment_signals,
reduced_cancellation_flexibility=reduced_cancellation_flexibility,
)
def _baseline_configuration(configuration: PricingConfiguration) -> PricingConfiguration:
return PricingConfiguration(
model=configuration.model,
segment=configuration.segment,
expected_usage_units=configuration.expected_usage_units,
expected_usage_variance_pct=configuration.expected_usage_variance_pct,
allocated_fixed_cost=configuration.allocated_fixed_cost,
direct_cost_amount=configuration.direct_cost_amount,
unit_cost=configuration.unit_cost,
support_cost=configuration.support_cost,
onboarding_cost=configuration.onboarding_cost,
risk_cost=configuration.risk_cost,
payment_fee_fixed=configuration.payment_fee_fixed,
payment_fee_rate_pct=configuration.payment_fee_rate_pct,
commitment_terms=default_commitment_terms(configuration.model),
)
def _segment_eligibility(
configuration: PricingConfiguration,
_policy: BoundaryPolicy,
_metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
if not configuration.segment or not configuration.model.eligibility:
return ConstraintResult(
id="segment-eligibility",
title="Segment eligibility",
severity="hard",
status="pass",
summary="Segment eligibility not restrictive for this evaluation.",
reason="No customer segment was supplied or the model declares no eligibility list.",
)
if configuration.segment in configuration.model.eligibility:
return ConstraintResult(
id="segment-eligibility",
title="Segment eligibility",
severity="hard",
status="pass",
summary=f"Segment '{configuration.segment}' is eligible.",
reason="The supplied customer segment is listed in the model eligibility rules.",
actual_value=configuration.segment,
)
return ConstraintResult(
id="segment-eligibility",
title="Segment eligibility",
severity="hard",
status="fail",
summary=f"Segment '{configuration.segment}' is not eligible for this model.",
reason="The model declares an explicit eligibility list and the supplied segment is outside it.",
actual_value=configuration.segment,
details={"eligible_segments": list(configuration.model.eligibility)},
suggested_action="Choose an eligible segment or use a different pricing model.",
)
def _usage_variance_limit(
configuration: PricingConfiguration,
policy: BoundaryPolicy,
_metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
actual = _percent(configuration.expected_usage_variance_pct)
if actual <= policy.max_expected_usage_variance_pct:
return ConstraintResult(
id="usage-variance-limit",
title="Usage variance limit",
severity="hard",
status="pass",
summary=f"Expected usage variance {actual}% is within the allowed range.",
reason="The scenario stays inside the configured usage-variance guardrail.",
actual_value=actual,
threshold_value=policy.max_expected_usage_variance_pct,
unit="percent",
)
return ConstraintResult(
id="usage-variance-limit",
title="Usage variance limit",
severity="hard",
status="fail",
summary=f"Expected usage variance {actual}% exceeds the allowed {policy.max_expected_usage_variance_pct}%.",
reason="High-variance usage invalidates the pricing configuration until the seller widens the guardrail or changes the package.",
actual_value=actual,
threshold_value=policy.max_expected_usage_variance_pct,
unit="percent",
suggested_action="Reduce exposure to volatile usage or tighten the included-usage assumptions.",
)
def _payment_fee_limit(
_configuration: PricingConfiguration,
policy: BoundaryPolicy,
metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
if metrics.payment_fee_pct <= policy.max_payment_fee_pct:
return ConstraintResult(
id="payment-fee-limit",
title="Payment fee limit",
severity="hard",
status="pass",
summary=f"Payment fees consume {metrics.payment_fee_pct}% of revenue, within policy.",
reason="Payment-provider fees remain inside the configured coverage ceiling.",
actual_value=metrics.payment_fee_pct,
threshold_value=policy.max_payment_fee_pct,
unit="percent",
)
return ConstraintResult(
id="payment-fee-limit",
title="Payment fee limit",
severity="hard",
status="fail",
summary=f"Payment fees consume {metrics.payment_fee_pct}% of revenue, above the {policy.max_payment_fee_pct}% ceiling.",
reason="The pricing configuration leaves too little contribution margin after provider fees.",
actual_value=metrics.payment_fee_pct,
threshold_value=policy.max_payment_fee_pct,
unit="percent",
suggested_action="Increase revenue, reduce fee burden, or change the collection method.",
)
def _cost_floor_coverage(
_configuration: PricingConfiguration,
_policy: BoundaryPolicy,
metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
if metrics.monthly_margin >= Decimal("0"):
return ConstraintResult(
id="cost-floor-coverage",
title="Cost floor coverage",
severity="hard",
status="pass",
summary=f"Monthly revenue clears the cost floor by {metrics.monthly_margin} {metrics.currency}.",
reason="Expected revenue covers allocated fixed cost, variable cost, and payment fees.",
actual_value=metrics.monthly_margin,
threshold_value=Decimal("0.00"),
unit=metrics.currency,
)
return ConstraintResult(
id="cost-floor-coverage",
title="Cost floor coverage",
severity="hard",
status="fail",
summary=f"Monthly revenue misses the cost floor by {abs(metrics.monthly_margin)} {metrics.currency}.",
reason=f"At least {metrics.cost_floor_revenue} {metrics.currency} monthly revenue is required to break even under these assumptions.",
actual_value=metrics.monthly_margin,
threshold_value=Decimal("0.00"),
unit=metrics.currency,
suggested_action="Raise price, lower cost, or add stronger commitments before offering this configuration.",
)
def _minimum_margin(
_configuration: PricingConfiguration,
policy: BoundaryPolicy,
metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
if metrics.margin_pct >= policy.minimum_margin_pct:
return ConstraintResult(
id="minimum-margin",
title="Minimum margin",
severity="hard",
status="pass",
summary=f"Margin {metrics.margin_pct}% satisfies the hard minimum.",
reason="The configuration meets the seller's minimum margin boundary.",
actual_value=metrics.margin_pct,
threshold_value=policy.minimum_margin_pct,
unit="percent",
)
return ConstraintResult(
id="minimum-margin",
title="Minimum margin",
severity="hard",
status="fail",
summary=f"Margin {metrics.margin_pct}% is below the hard minimum of {policy.minimum_margin_pct}%.",
reason=f"At least {metrics.minimum_margin_revenue} {metrics.currency} monthly revenue is required to satisfy the minimum margin boundary.",
actual_value=metrics.margin_pct,
threshold_value=policy.minimum_margin_pct,
unit="percent",
suggested_action="Increase price, reduce costs, or add commitment-backed protection.",
)
def _target_margin_approval(
_configuration: PricingConfiguration,
policy: BoundaryPolicy,
metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
if metrics.margin_pct >= policy.target_margin_pct:
return ConstraintResult(
id="target-margin-approval",
title="Target margin approval threshold",
severity="soft",
status="pass",
summary=f"Margin {metrics.margin_pct}% satisfies the target margin threshold.",
reason="No sales or pricing approval is required on margin grounds.",
actual_value=metrics.margin_pct,
threshold_value=policy.target_margin_pct,
unit="percent",
)
return ConstraintResult(
id="target-margin-approval",
title="Target margin approval threshold",
severity="soft",
status="review",
summary=f"Margin {metrics.margin_pct}% is below the target threshold of {policy.target_margin_pct}%.",
reason=f"The configuration is economically viable but falls short of the seller's preferred target margin of {policy.target_margin_pct}%.",
actual_value=metrics.margin_pct,
threshold_value=policy.target_margin_pct,
unit="percent",
suggested_action="Route through approval or improve economics before release.",
)
def _discount_exposure_limit(
_configuration: PricingConfiguration,
policy: BoundaryPolicy,
metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
if metrics.concession_pct <= policy.max_discount_pct:
return ConstraintResult(
id="discount-exposure-limit",
title="Discount exposure limit",
severity="hard",
status="pass",
summary=f"Economic concession {metrics.concession_pct}% stays inside the hard discount ceiling.",
reason="The configuration does not exceed the seller's maximum discount exposure.",
actual_value=metrics.concession_pct,
threshold_value=policy.max_discount_pct,
unit="percent",
)
return ConstraintResult(
id="discount-exposure-limit",
title="Discount exposure limit",
severity="hard",
status="fail",
summary=f"Economic concession {metrics.concession_pct}% exceeds the hard discount ceiling of {policy.max_discount_pct}%.",
reason="The proposed economics reduce seller value too far relative to the model baseline.",
actual_value=metrics.concession_pct,
threshold_value=policy.max_discount_pct,
unit="percent",
suggested_action="Reduce the concession or offset it with a materially stronger commitment structure.",
)
def _discount_approval_threshold(
_configuration: PricingConfiguration,
policy: BoundaryPolicy,
metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
if metrics.concession_pct <= policy.approval_discount_pct:
return ConstraintResult(
id="discount-approval-threshold",
title="Discount approval threshold",
severity="soft",
status="pass",
summary=f"Economic concession {metrics.concession_pct}% is inside the self-serve threshold.",
reason="No extra approval is needed for discount exposure.",
actual_value=metrics.concession_pct,
threshold_value=policy.approval_discount_pct,
unit="percent",
)
return ConstraintResult(
id="discount-approval-threshold",
title="Discount approval threshold",
severity="soft",
status="review",
summary=f"Economic concession {metrics.concession_pct}% exceeds the self-serve threshold of {policy.approval_discount_pct}%.",
reason="The configuration may still be acceptable, but it now requires seller approval instead of self-serve acceptance.",
actual_value=metrics.concession_pct,
threshold_value=policy.approval_discount_pct,
unit="percent",
suggested_action="Escalate for approval or reduce the concession magnitude.",
)
def _commitment_backed_concession(
_configuration: PricingConfiguration,
_policy: BoundaryPolicy,
metrics: PricingMetrics,
_baseline: PricingMetrics,
) -> ConstraintResult:
if metrics.concession_value <= Decimal("0"):
return ConstraintResult(
id="commitment-backed-concession",
title="Commitment-backed concession",
severity="hard",
status="pass",
summary="No economic concession was introduced relative to the model baseline.",
reason="The configuration does not weaken seller economics versus the baseline assumptions.",
actual_value=metrics.concession_value,
threshold_value=Decimal("0.00"),
unit=metrics.currency,
)
if metrics.meaningful_commitment_signals:
return ConstraintResult(
id="commitment-backed-concession",
title="Commitment-backed concession",
severity="hard",
status="pass",
summary="The economic concession is backed by explicit commitments.",
reason="The configuration introduces weaker unit economics, but it also adds meaningful seller protections.",
actual_value=metrics.concession_value,
threshold_value=Decimal("0.00"),
unit=metrics.currency,
details={"signals": list(metrics.meaningful_commitment_signals)},
)
return ConstraintResult(
id="commitment-backed-concession",
title="Commitment-backed concession",
severity="hard",
status="fail",
summary="The configuration introduces weaker seller economics without an offsetting commitment.",
reason="Discounts and improved customer unit economics must be tied to enforceable or economically meaningful commitments.",
actual_value=metrics.concession_value,
threshold_value=Decimal("0.00"),
unit=metrics.currency,
suggested_action="Add minimum turnover, prepayment, guaranteed fees, or a materially longer contract before offering this concession.",
)
def default_constraints() -> tuple[BoundaryConstraint, ...]:
return (
BoundaryConstraint("segment-eligibility", "Segment eligibility", "hard", _segment_eligibility),
BoundaryConstraint("usage-variance-limit", "Usage variance limit", "hard", _usage_variance_limit),
BoundaryConstraint("payment-fee-limit", "Payment fee limit", "hard", _payment_fee_limit),
BoundaryConstraint("cost-floor-coverage", "Cost floor coverage", "hard", _cost_floor_coverage),
BoundaryConstraint("minimum-margin", "Minimum margin", "hard", _minimum_margin),
BoundaryConstraint("target-margin-approval", "Target margin approval threshold", "soft", _target_margin_approval),
BoundaryConstraint("discount-exposure-limit", "Discount exposure limit", "hard", _discount_exposure_limit),
BoundaryConstraint("discount-approval-threshold", "Discount approval threshold", "soft", _discount_approval_threshold),
BoundaryConstraint("commitment-backed-concession", "Commitment-backed concession", "hard", _commitment_backed_concession),
)
def _configuration_view(configuration: PricingConfiguration, metrics: PricingMetrics) -> dict[str, Any]:
return {
"segment": configuration.segment,
"currency": metrics.currency,
"access_fee_amount": metrics.access_fee_amount,
"included_units": metrics.included_units,
"usage_unit_price": metrics.usage_unit_price,
"expected_usage_units": metrics.expected_usage_units,
"expected_usage_variance_pct": _percent(configuration.expected_usage_variance_pct),
"allocated_fixed_cost": metrics.allocated_fixed_cost,
"direct_cost_amount": metrics.direct_cost_amount,
"unit_cost": metrics.unit_cost,
"support_cost": metrics.support_cost,
"onboarding_cost": metrics.onboarding_cost,
"risk_cost": metrics.risk_cost,
"payment_fee_fixed": _money(configuration.payment_fee_fixed),
"payment_fee_rate_pct": _percent(configuration.payment_fee_rate_pct),
"discount_amount": _money(configuration.discount_amount),
"commitment_terms": {
"contract_duration_months": metrics.contract_duration_months,
"minimum_monthly_turnover": metrics.minimum_monthly_turnover,
"prepaid_amount": metrics.prepaid_amount,
"guaranteed_platform_fee": metrics.guaranteed_platform_fee,
"customer_funded_onboarding": metrics.customer_funded_onboarding,
"reduced_cancellation_flexibility": metrics.reduced_cancellation_flexibility,
},
}
def validate_pricing_configuration(
configuration: PricingConfiguration,
policy: BoundaryPolicy,
constraints: tuple[BoundaryConstraint, ...] | None = None,
) -> ValidationResult:
baseline_metrics = _build_metrics(_baseline_configuration(configuration), policy)
metrics = _build_metrics(configuration, policy, baseline_metrics=baseline_metrics)
results = tuple(
constraint.evaluator(configuration, policy, metrics, baseline_metrics)
for constraint in (constraints or default_constraints())
)
hard_failures = [result for result in results if result.status == "fail" and result.severity == "hard"]
soft_reviews = [result for result in results if result.status == "review"]
valid = not hard_failures
requires_approval = valid and bool(soft_reviews)
if hard_failures:
decision: ValidationDecision = "rejected"
summary = "Rejected: " + ", ".join(result.title for result in hard_failures) + "."
elif soft_reviews:
decision = "requires_approval"
summary = "Approval required: " + ", ".join(result.title for result in soft_reviews) + "."
else:
decision = "accepted"
summary = "Accepted: all boundary constraints passed."
return ValidationResult(
model_id=configuration.model.id,
model_name=configuration.model.name,
decision=decision,
valid=valid,
requires_approval=requires_approval,
summary=summary,
configuration=_configuration_view(configuration, metrics),
metrics=metrics,
policy=policy,
constraints=results,
)

View File

@@ -0,0 +1,496 @@
from __future__ import annotations
from dataclasses import dataclass, field, replace
from decimal import Decimal, ROUND_HALF_UP
from typing import Any
from .boundary_engine import (
BoundaryPolicy,
PricingConfiguration,
ValidationResult,
validate_pricing_configuration,
)
TWOPLACES = Decimal("0.01")
PCTPLACES = Decimal("0.1")
def _money(value: Decimal) -> Decimal:
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
def _percent(value: Decimal) -> Decimal:
return value.quantize(PCTPLACES, rounding=ROUND_HALF_UP)
def _decimal(value: Decimal | str | int | float | None) -> Decimal:
if value in (None, ""):
return Decimal("0")
return Decimal(str(value))
@dataclass(frozen=True)
class ComparableCustomerProfile:
id: str
name: str
segment: str
eligible_model_ids: tuple[str, ...] = ()
members_per_customer: int = 1
expected_monthly_usage_units: Decimal = Decimal("0")
usage_variance_pct: Decimal = Decimal("0")
monthly_churn_pct: Decimal = Decimal("0")
monthly_default_pct: Decimal = Decimal("0")
monthly_support_cost: Decimal = Decimal("0")
monthly_risk_cost: Decimal = Decimal("0")
acquisition_cost: Decimal = Decimal("0")
upfront_investment_cost: Decimal = Decimal("0")
allocated_fixed_cost: Decimal | None = None
notes: str = ""
@dataclass(frozen=True)
class LTVPolicy:
horizon_months: int = 24
monthly_discount_rate_pct: Decimal = Decimal("1")
required_improvement_factor: Decimal = Decimal("1.05")
@dataclass(frozen=True)
class SensitivityCase:
id: str
name: str
usage_multiplier: Decimal = Decimal("1")
usage_variance_delta_pct: Decimal = Decimal("0")
monthly_churn_delta_pct: Decimal = Decimal("0")
monthly_default_delta_pct: Decimal = Decimal("0")
monthly_support_cost_delta: Decimal = Decimal("0")
monthly_risk_cost_delta: Decimal = Decimal("0")
@dataclass(frozen=True)
class ComparableLTVEstimate:
model_id: str
model_name: str
validation: ValidationResult
average_comparable_customer_lifetime_value: Decimal
discounted_margin_ltv: Decimal
expected_lifetime_months: Decimal
payback_months: int | None
adjusted_monthly_churn_pct: Decimal
adjusted_monthly_default_pct: Decimal
acquisition_cost: Decimal
upfront_investment_cost: Decimal
seller_cost_recovery_months: int | None
assumptions: dict[str, Any]
explanation: str
@dataclass(frozen=True)
class SensitivityOutcome:
case_id: str
case_name: str
average_comparable_customer_lifetime_value: Decimal
delta_vs_base: Decimal
delta_vs_base_pct: Decimal | None
decision: str
@dataclass(frozen=True)
class PricingModelComparison:
model_id: str
model_name: str
model_type: str
status: str
validation_decision: str
valid: bool
requires_approval: bool
average_comparable_customer_lifetime_value: Decimal
expected_lifetime_months: Decimal
base_monthly_margin: Decimal
base_margin_pct: Decimal
payment_fee_pct: Decimal
contract_duration_months: int
reference_model_id: str | None
passes_required_improvement: bool
required_improvement_threshold: Decimal | None
improvement_vs_reference_pct: Decimal | None
sensitivity: tuple[SensitivityOutcome, ...]
sensitivity_floor_ltv: Decimal
sensitivity_ceiling_ltv: Decimal
key_drivers: tuple[str, ...]
comparison_summary: str
@dataclass(frozen=True)
class PricingComparison:
profile: ComparableCustomerProfile
policy: LTVPolicy
boundary_policy: BoundaryPolicy
reference_model_id: str | None
best_ltv_model_id: str | None
best_valid_model_id: str | None
comparisons: tuple[PricingModelComparison, ...]
notes: tuple[str, ...]
def default_sensitivity_cases() -> tuple[SensitivityCase, ...]:
return (
SensitivityCase(
id="usage-downside",
name="Usage downside",
usage_multiplier=Decimal("0.75"),
monthly_churn_delta_pct=Decimal("1.5"),
monthly_risk_cost_delta=Decimal("0.05"),
),
SensitivityCase(
id="usage-upside",
name="Usage upside",
usage_multiplier=Decimal("1.35"),
usage_variance_delta_pct=Decimal("10"),
),
SensitivityCase(
id="risk-spike",
name="Risk spike",
usage_multiplier=Decimal("1.00"),
monthly_churn_delta_pct=Decimal("3.0"),
monthly_default_delta_pct=Decimal("1.0"),
monthly_support_cost_delta=Decimal("0.25"),
monthly_risk_cost_delta=Decimal("0.20"),
),
)
def _risk_multiplier_for_default(validation: ValidationResult) -> Decimal:
metrics = validation.metrics
multiplier = Decimal("1")
if metrics.prepaid_amount >= metrics.effective_monthly_revenue and metrics.prepaid_amount > Decimal("0"):
multiplier *= Decimal("0.50")
if (
metrics.guaranteed_platform_fee >= metrics.effective_monthly_revenue
and metrics.guaranteed_platform_fee > Decimal("0")
):
multiplier *= Decimal("0.75")
return multiplier
def _risk_multiplier_for_churn(validation: ValidationResult) -> Decimal:
metrics = validation.metrics
multiplier = Decimal("1")
if metrics.reduced_cancellation_flexibility:
multiplier *= Decimal("0.85")
return multiplier
def _discount_rate(policy: LTVPolicy) -> Decimal:
return Decimal("1") + (policy.monthly_discount_rate_pct / Decimal("100"))
def required_improvement_threshold(reference_ltv: Decimal, factor: Decimal) -> Decimal:
if reference_ltv >= Decimal("0"):
return _money(reference_ltv * factor)
improvement = abs(reference_ltv) * (factor - Decimal("1"))
return _money(reference_ltv + improvement)
def _pct_delta(candidate: Decimal, reference: Decimal) -> Decimal | None:
if reference == Decimal("0"):
return None
return _percent(((candidate - reference) / abs(reference)) * Decimal("100"))
def estimate_comparable_customer_ltv(
configuration: PricingConfiguration,
profile: ComparableCustomerProfile,
boundary_policy: BoundaryPolicy,
policy: LTVPolicy,
) -> ComparableLTVEstimate:
validation = validate_pricing_configuration(configuration, boundary_policy)
base_churn_pct = max(profile.monthly_churn_pct, Decimal("0"))
base_default_pct = max(profile.monthly_default_pct, Decimal("0"))
adjusted_churn_pct = _percent(base_churn_pct * _risk_multiplier_for_churn(validation))
adjusted_default_pct = _percent(base_default_pct * _risk_multiplier_for_default(validation))
monthly_margin = validation.metrics.monthly_margin
acquisition_cost = _money(profile.acquisition_cost)
upfront_investment_cost = _money(profile.upfront_investment_cost)
committed_months = max(validation.metrics.contract_duration_months, 1)
survival = Decimal("1")
expected_lifetime_months = Decimal("0")
discounted_margin = Decimal("0")
cumulative_discounted_margin = Decimal("0")
payback_months: int | None = None
recovery_months: int | None = None
hurdle = acquisition_cost + upfront_investment_cost
adjusted_churn_rate = adjusted_churn_pct / Decimal("100")
adjusted_default_rate = adjusted_default_pct / Decimal("100")
discount_rate = _discount_rate(policy)
for month in range(1, policy.horizon_months + 1):
expected_lifetime_months += survival
discounted_month_margin = (monthly_margin * survival) / (discount_rate ** month)
discounted_margin += discounted_month_margin
cumulative_discounted_margin += discounted_month_margin
if payback_months is None and cumulative_discounted_margin >= hurdle:
payback_months = month
if recovery_months is None and cumulative_discounted_margin >= Decimal("0"):
recovery_months = month
if month < committed_months:
survival *= Decimal("1") - adjusted_default_rate
else:
survival *= (Decimal("1") - adjusted_default_rate) * (Decimal("1") - adjusted_churn_rate)
average_ltv = _money(discounted_margin - hurdle)
explanation = (
f"Monthly margin {validation.metrics.monthly_margin} {validation.metrics.currency}, "
f"expected lifetime {expected_lifetime_months.quantize(Decimal('0.1'))} months, "
f"discounted seller LTV {average_ltv} {validation.metrics.currency}."
)
return ComparableLTVEstimate(
model_id=configuration.model.id,
model_name=configuration.model.name,
validation=validation,
average_comparable_customer_lifetime_value=average_ltv,
discounted_margin_ltv=_money(discounted_margin),
expected_lifetime_months=expected_lifetime_months.quantize(Decimal("0.1")),
payback_months=payback_months,
adjusted_monthly_churn_pct=adjusted_churn_pct,
adjusted_monthly_default_pct=adjusted_default_pct,
acquisition_cost=acquisition_cost,
upfront_investment_cost=upfront_investment_cost,
seller_cost_recovery_months=recovery_months,
assumptions={
"horizon_months": policy.horizon_months,
"monthly_discount_rate_pct": _percent(policy.monthly_discount_rate_pct),
"base_monthly_churn_pct": _percent(base_churn_pct),
"base_monthly_default_pct": _percent(base_default_pct),
"committed_months": committed_months,
},
explanation=explanation,
)
def _apply_sensitivity(
configuration: PricingConfiguration,
profile: ComparableCustomerProfile,
case: SensitivityCase,
) -> tuple[PricingConfiguration, ComparableCustomerProfile]:
return (
replace(
configuration,
expected_usage_units=_money(configuration.expected_usage_units * case.usage_multiplier),
expected_usage_variance_pct=max(
configuration.expected_usage_variance_pct + case.usage_variance_delta_pct,
Decimal("0"),
),
support_cost=max(configuration.support_cost + case.monthly_support_cost_delta, Decimal("0")),
risk_cost=max(configuration.risk_cost + case.monthly_risk_cost_delta, Decimal("0")),
),
replace(
profile,
monthly_churn_pct=max(profile.monthly_churn_pct + case.monthly_churn_delta_pct, Decimal("0")),
monthly_default_pct=max(profile.monthly_default_pct + case.monthly_default_delta_pct, Decimal("0")),
),
)
def select_reference_estimate(
estimates: list[ComparableLTVEstimate],
eligible_model_ids: tuple[str, ...] = (),
) -> ComparableLTVEstimate | None:
candidates = estimates
if eligible_model_ids:
allowed = set(eligible_model_ids)
candidates = [estimate for estimate in estimates if estimate.model_id in allowed]
valid = [estimate for estimate in candidates if estimate.validation.valid]
if valid:
return max(valid, key=lambda item: item.average_comparable_customer_lifetime_value)
if candidates:
return max(candidates, key=lambda item: item.average_comparable_customer_lifetime_value)
return None
def _key_drivers(
estimate: ComparableLTVEstimate,
reference: ComparableLTVEstimate | None,
) -> tuple[str, ...]:
drivers: list[str] = []
metrics = estimate.validation.metrics
if reference is None:
return ("no_reference_model_available",)
ref_metrics = reference.validation.metrics
if metrics.monthly_margin > ref_metrics.monthly_margin:
drivers.append("higher_monthly_margin")
if estimate.expected_lifetime_months > reference.expected_lifetime_months:
drivers.append("longer_expected_lifetime")
if metrics.contract_duration_months > ref_metrics.contract_duration_months:
drivers.append("longer_commitment_window")
if metrics.payment_fee_pct < ref_metrics.payment_fee_pct:
drivers.append("lower_payment_fee_burden")
if metrics.minimum_monthly_turnover > ref_metrics.minimum_monthly_turnover:
drivers.append("stronger_revenue_commitment")
if not drivers:
drivers.append("reference_like_economics")
return tuple(drivers)
def _comparison_summary(
estimate: ComparableLTVEstimate,
reference: ComparableLTVEstimate | None,
passes_required_improvement: bool,
threshold: Decimal | None,
) -> str:
if reference is None:
return f"{estimate.model_name}: no reference model available for improvement comparison."
improvement = _pct_delta(
estimate.average_comparable_customer_lifetime_value,
reference.average_comparable_customer_lifetime_value,
)
if estimate.model_id == reference.model_id:
return f"{estimate.model_name}: reference model for this comparable-customer segment."
if passes_required_improvement:
return (
f"{estimate.model_name}: LTV {estimate.average_comparable_customer_lifetime_value} "
f"meets threshold {threshold} with improvement {improvement}%."
)
return (
f"{estimate.model_name}: LTV {estimate.average_comparable_customer_lifetime_value} "
f"misses threshold {threshold} with improvement {improvement}%."
)
def compare_pricing_configurations(
configurations: list[PricingConfiguration],
profile: ComparableCustomerProfile,
boundary_policy: BoundaryPolicy,
policy: LTVPolicy,
sensitivity_cases: tuple[SensitivityCase, ...] | None = None,
) -> PricingComparison:
base_estimates = [
estimate_comparable_customer_ltv(configuration, profile, boundary_policy, policy)
for configuration in configurations
]
reference = select_reference_estimate(base_estimates, profile.eligible_model_ids)
comparisons: list[PricingModelComparison] = []
for configuration, estimate in zip(configurations, base_estimates, strict=True):
outcomes: list[SensitivityOutcome] = []
for case in sensitivity_cases or default_sensitivity_cases():
scenario_configuration, scenario_profile = _apply_sensitivity(configuration, profile, case)
scenario_estimate = estimate_comparable_customer_ltv(
scenario_configuration,
scenario_profile,
boundary_policy,
policy,
)
delta_vs_base = _money(
scenario_estimate.average_comparable_customer_lifetime_value
- estimate.average_comparable_customer_lifetime_value
)
outcomes.append(
SensitivityOutcome(
case_id=case.id,
case_name=case.name,
average_comparable_customer_lifetime_value=scenario_estimate.average_comparable_customer_lifetime_value,
delta_vs_base=delta_vs_base,
delta_vs_base_pct=_pct_delta(
scenario_estimate.average_comparable_customer_lifetime_value,
estimate.average_comparable_customer_lifetime_value,
),
decision=scenario_estimate.validation.decision,
)
)
threshold: Decimal | None = None
passes_required_improvement = True
if reference is not None and estimate.model_id != reference.model_id:
threshold = required_improvement_threshold(
reference.average_comparable_customer_lifetime_value,
policy.required_improvement_factor,
)
passes_required_improvement = (
estimate.average_comparable_customer_lifetime_value >= threshold
)
improvement_vs_reference_pct = (
Decimal("0.0")
if reference is not None and estimate.model_id == reference.model_id
else (
_pct_delta(
estimate.average_comparable_customer_lifetime_value,
reference.average_comparable_customer_lifetime_value,
)
if reference is not None
else None
)
)
sensitivity_floor = min(
[estimate.average_comparable_customer_lifetime_value]
+ [item.average_comparable_customer_lifetime_value for item in outcomes]
)
sensitivity_ceiling = max(
[estimate.average_comparable_customer_lifetime_value]
+ [item.average_comparable_customer_lifetime_value for item in outcomes]
)
comparisons.append(
PricingModelComparison(
model_id=estimate.model_id,
model_name=estimate.model_name,
model_type=configuration.model.model_type,
status=configuration.model.status,
validation_decision=estimate.validation.decision,
valid=estimate.validation.valid,
requires_approval=estimate.validation.requires_approval,
average_comparable_customer_lifetime_value=estimate.average_comparable_customer_lifetime_value,
expected_lifetime_months=estimate.expected_lifetime_months,
base_monthly_margin=estimate.validation.metrics.monthly_margin,
base_margin_pct=estimate.validation.metrics.margin_pct,
payment_fee_pct=estimate.validation.metrics.payment_fee_pct,
contract_duration_months=estimate.validation.metrics.contract_duration_months,
reference_model_id=reference.model_id if reference else None,
passes_required_improvement=passes_required_improvement,
required_improvement_threshold=threshold,
improvement_vs_reference_pct=improvement_vs_reference_pct,
sensitivity=tuple(outcomes),
sensitivity_floor_ltv=sensitivity_floor,
sensitivity_ceiling_ltv=sensitivity_ceiling,
key_drivers=_key_drivers(estimate, reference),
comparison_summary=_comparison_summary(
estimate,
reference,
passes_required_improvement,
threshold,
),
)
)
best_ltv = max(comparisons, key=lambda item: item.average_comparable_customer_lifetime_value, default=None)
valid_comparisons = [item for item in comparisons if item.valid]
best_valid = max(valid_comparisons, key=lambda item: item.average_comparable_customer_lifetime_value, default=None)
notes = (
"Comparable-customer LTV uses discounted expected seller margin over a finite horizon.",
"Reference selection prefers valid eligible predefined models with the highest seller LTV.",
"Negative reference LTV uses additive improvement semantics so tuned models must become less negative, not more negative.",
)
return PricingComparison(
profile=profile,
policy=policy,
boundary_policy=boundary_policy,
reference_model_id=reference.model_id if reference else None,
best_ltv_model_id=best_ltv.model_id if best_ltv else None,
best_valid_model_id=best_valid.model_id if best_valid else None,
comparisons=tuple(comparisons),
notes=notes,
)

View File

@@ -0,0 +1,511 @@
from __future__ import annotations
from dataclasses import dataclass, replace
from decimal import Decimal, ROUND_HALF_UP
from typing import Literal
from .boundary_engine import (
BoundaryPolicy,
CommitmentTerms,
ConstraintResult,
PricingConfiguration,
ValidationResult,
)
from .comparable_ltv import (
ComparableCustomerProfile,
ComparableLTVEstimate,
LTVPolicy,
estimate_comparable_customer_ltv,
required_improvement_threshold,
select_reference_estimate,
)
SolverPreference = Literal["lower_usage_price", "seller_ltv"]
ApprovalMode = Literal["self_serve_only", "allow_approval"]
TuningDecision = Literal["accepted", "requires_approval", "rejected"]
TWOPLACES = Decimal("0.01")
def _money(value: Decimal) -> Decimal:
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
def _usage_component(configuration: PricingConfiguration):
return next(
(component for component in configuration.model.charge_components if component.kind == "usage"),
None,
)
def _default_usage_unit_price(configuration: PricingConfiguration) -> Decimal:
usage_component = _usage_component(configuration)
if configuration.usage_unit_price is not None:
return configuration.usage_unit_price
if usage_component and usage_component.unit_price is not None:
return usage_component.unit_price
for parameter in configuration.model.tunable_parameters:
if parameter.key == "overage_unit_price" and parameter.default_value not in (None, ""):
return Decimal(str(parameter.default_value))
return Decimal("0")
def _percent_delta(candidate: Decimal, reference: Decimal) -> Decimal | None:
if reference == Decimal("0"):
return None
return _money(((candidate - reference) / abs(reference)) * Decimal("100"))
@dataclass(frozen=True)
class UsagePriceSearchPolicy:
min_usage_unit_price: Decimal | None = None
max_usage_unit_price: Decimal | None = None
usage_unit_price_step: Decimal = Decimal("0.0001")
max_usage_price_multiplier: Decimal = Decimal("4")
@dataclass(frozen=True)
class CustomerTuningRequest:
included_units: Decimal | None = None
contract_duration_months: int | None = None
minimum_monthly_turnover: Decimal = Decimal("0")
prepaid_amount: Decimal = Decimal("0")
guaranteed_platform_fee: Decimal = Decimal("0")
customer_funded_onboarding: Decimal = Decimal("0")
reduced_cancellation_flexibility: bool | None = None
preference: SolverPreference = "lower_usage_price"
approval_mode: ApprovalMode = "self_serve_only"
@dataclass(frozen=True)
class CustomerTuningOutcome:
model_id: str
model_name: str
decision: TuningDecision
valid: bool
requires_approval: bool
preference: SolverPreference
approval_mode: ApprovalMode
request: CustomerTuningRequest
solved_configuration: dict[str, object]
solved_usage_unit_price: Decimal
reference_model_id: str | None
reference_model_name: str | None
reference_ltv: Decimal | None
required_improvement_threshold: Decimal | None
average_comparable_customer_lifetime_value: Decimal
improvement_vs_reference_pct: Decimal | None
passes_required_improvement: bool
evaluated_candidates: int
tradeoffs: tuple[str, ...]
binding_constraints: tuple[ConstraintResult, ...]
validation: ValidationResult
explanation: str
@dataclass(frozen=True)
class _CandidateAssessment:
configuration: PricingConfiguration
estimate: ComparableLTVEstimate
decision: TuningDecision
passes_required_improvement: bool
improvement_vs_reference_pct: Decimal | None
def _price_range(
configuration: PricingConfiguration,
search_policy: UsagePriceSearchPolicy,
) -> tuple[Decimal, ...]:
step = search_policy.usage_unit_price_step
if step <= Decimal("0"):
raise ValueError("usage_unit_price_step must be positive")
default_usage_price = _default_usage_unit_price(configuration)
min_usage_price = search_policy.min_usage_unit_price
if min_usage_price is None:
min_usage_price = max(configuration.unit_cost, default_usage_price / Decimal("10"), step)
max_usage_price = search_policy.max_usage_unit_price
if max_usage_price is None:
base = default_usage_price if default_usage_price > Decimal("0") else step
max_usage_price = max(min_usage_price, base * search_policy.max_usage_price_multiplier)
if max_usage_price < min_usage_price:
max_usage_price = min_usage_price
values: list[Decimal] = []
current = min_usage_price
while current <= max_usage_price:
values.append(current)
current += step
if not values or values[-1] != max_usage_price:
values.append(max_usage_price)
return tuple(dict.fromkeys(values))
def _resolved_search_policy(
configuration: PricingConfiguration,
request: CustomerTuningRequest,
search_policy: UsagePriceSearchPolicy | None,
) -> UsagePriceSearchPolicy:
policy = search_policy or UsagePriceSearchPolicy()
if request.preference != "lower_usage_price" or policy.max_usage_unit_price is not None:
return policy
return replace(
policy,
max_usage_unit_price=_default_usage_unit_price(configuration),
)
def _commitment_terms(
base_terms: CommitmentTerms,
request: CustomerTuningRequest,
) -> CommitmentTerms:
return replace(
base_terms,
contract_duration_months=(
request.contract_duration_months
if request.contract_duration_months is not None
else base_terms.contract_duration_months
),
minimum_monthly_turnover=request.minimum_monthly_turnover,
prepaid_amount=request.prepaid_amount,
guaranteed_platform_fee=request.guaranteed_platform_fee,
customer_funded_onboarding=request.customer_funded_onboarding,
reduced_cancellation_flexibility=(
request.reduced_cancellation_flexibility
if request.reduced_cancellation_flexibility is not None
else base_terms.reduced_cancellation_flexibility
),
)
def _candidate_configuration(
base_configuration: PricingConfiguration,
request: CustomerTuningRequest,
usage_unit_price: Decimal,
) -> PricingConfiguration:
return replace(
base_configuration,
included_units=(
request.included_units
if request.included_units is not None
else base_configuration.included_units
),
usage_unit_price=usage_unit_price,
commitment_terms=_commitment_terms(base_configuration.commitment_terms, request),
)
def _candidate_decision(
validation: ValidationResult,
passes_required_improvement: bool,
approval_mode: ApprovalMode,
) -> TuningDecision:
if not validation.valid or not passes_required_improvement:
return "rejected"
if validation.requires_approval:
return "requires_approval" if approval_mode == "allow_approval" else "rejected"
return "accepted"
def _headroom_by_constraint(
configuration: PricingConfiguration,
validation: ValidationResult,
) -> dict[str, Decimal]:
metrics = validation.metrics
policy = validation.policy
return {
"usage-variance-limit": policy.max_expected_usage_variance_pct - configuration.expected_usage_variance_pct,
"payment-fee-limit": policy.max_payment_fee_pct - metrics.payment_fee_pct,
"cost-floor-coverage": metrics.monthly_margin,
"minimum-margin": metrics.margin_pct - policy.minimum_margin_pct,
"target-margin-approval": metrics.margin_pct - policy.target_margin_pct,
"discount-exposure-limit": policy.max_discount_pct - metrics.concession_pct,
"discount-approval-threshold": policy.approval_discount_pct - metrics.concession_pct,
}
def _binding_constraints(
configuration: PricingConfiguration,
validation: ValidationResult,
) -> tuple[ConstraintResult, ...]:
flagged = tuple(result for result in validation.constraints if result.status != "pass")
if flagged:
return flagged
headroom = _headroom_by_constraint(configuration, validation)
ordered_ids = [
constraint_id
for constraint_id, _headroom in sorted(headroom.items(), key=lambda item: item[1])
if constraint_id in {result.id for result in validation.constraints}
]
selected_ids = ordered_ids[:2]
if not selected_ids:
return ()
return tuple(
result for result in validation.constraints if result.id in selected_ids
)
def _tradeoffs(
base_configuration: PricingConfiguration,
candidate_configuration: PricingConfiguration,
validation: ValidationResult,
) -> tuple[str, ...]:
tradeoffs: list[str] = []
if (
base_configuration.included_units is not None
and candidate_configuration.included_units is not None
and candidate_configuration.included_units < base_configuration.included_units
):
tradeoffs.append("lower_included_usage")
if (
base_configuration.included_units is not None
and candidate_configuration.included_units is not None
and candidate_configuration.included_units > base_configuration.included_units
):
tradeoffs.append("higher_included_usage")
if (
base_configuration.usage_unit_price is not None
and candidate_configuration.usage_unit_price is not None
and candidate_configuration.usage_unit_price < base_configuration.usage_unit_price
):
tradeoffs.append("lower_usage_price")
if (
base_configuration.usage_unit_price is not None
and candidate_configuration.usage_unit_price is not None
and candidate_configuration.usage_unit_price > base_configuration.usage_unit_price
):
tradeoffs.append("higher_usage_price")
baseline_duration = base_configuration.commitment_terms.contract_duration_months or 0
candidate_duration = validation.metrics.contract_duration_months
if candidate_duration > baseline_duration:
tradeoffs.append("longer_contract_duration")
if validation.metrics.minimum_monthly_turnover > Decimal("0"):
tradeoffs.append("minimum_monthly_turnover")
if validation.metrics.prepaid_amount > Decimal("0"):
tradeoffs.append("prepayment")
if validation.metrics.guaranteed_platform_fee > Decimal("0"):
tradeoffs.append("guaranteed_platform_fee")
if validation.metrics.customer_funded_onboarding > Decimal("0"):
tradeoffs.append("customer_funded_onboarding")
if validation.metrics.reduced_cancellation_flexibility:
tradeoffs.append("reduced_cancellation_flexibility")
for signal in validation.metrics.meaningful_commitment_signals:
if signal not in tradeoffs:
tradeoffs.append(signal)
return tuple(tradeoffs)
def _explanation(
assessment: _CandidateAssessment,
request: CustomerTuningRequest,
reference_estimate: ComparableLTVEstimate | None,
threshold: Decimal | None,
tradeoffs: tuple[str, ...],
binding_constraints: tuple[ConstraintResult, ...],
) -> str:
validation = assessment.estimate.validation
metrics = validation.metrics
if assessment.decision in {"accepted", "requires_approval"}:
outcome = (
"Accepted self-serve tuning"
if assessment.decision == "accepted"
else "Requires seller approval"
)
parts = [
f"{outcome} at {metrics.usage_unit_price} {metrics.currency} usage price.",
(
f"Comparable-customer LTV {assessment.estimate.average_comparable_customer_lifetime_value} "
f"{metrics.currency}"
),
]
if reference_estimate is not None and threshold is not None:
parts.append(
f"clears threshold {threshold} {metrics.currency} versus {reference_estimate.model_name}."
)
if tradeoffs:
parts.append("Trade-offs: " + ", ".join(tradeoffs) + ".")
return " ".join(parts)
failed_constraints = [result.title for result in binding_constraints if result.status == "fail"]
review_constraints = [result.title for result in binding_constraints if result.status == "review"]
parts = ["Rejected tuning request."]
if not assessment.passes_required_improvement and reference_estimate is not None and threshold is not None:
parts.append(
(
f"LTV {assessment.estimate.average_comparable_customer_lifetime_value} {metrics.currency} "
f"misses threshold {threshold} {metrics.currency} versus {reference_estimate.model_name}."
)
)
if failed_constraints:
parts.append("Hard blockers: " + ", ".join(failed_constraints) + ".")
if review_constraints and request.approval_mode == "self_serve_only":
parts.append("Self-serve blockers: " + ", ".join(review_constraints) + ".")
if tradeoffs:
parts.append("Attempted trade-offs: " + ", ".join(tradeoffs) + ".")
return " ".join(parts)
def _acceptable_candidates(
candidates: tuple[_CandidateAssessment, ...],
) -> tuple[_CandidateAssessment, ...]:
return tuple(candidate for candidate in candidates if candidate.decision in {"accepted", "requires_approval"})
def _candidate_sort_key(
candidate: _CandidateAssessment,
preference: SolverPreference,
) -> tuple[Decimal, Decimal]:
usage_price = candidate.estimate.validation.metrics.usage_unit_price
ltv = candidate.estimate.average_comparable_customer_lifetime_value
if preference == "lower_usage_price":
return (usage_price, -ltv)
return (-ltv, usage_price)
def _fallback_sort_key(
candidate: _CandidateAssessment,
preference: SolverPreference,
) -> tuple[int, int, int, Decimal, Decimal]:
usage_price = candidate.estimate.validation.metrics.usage_unit_price
ltv = candidate.estimate.average_comparable_customer_lifetime_value
return (
0 if candidate.passes_required_improvement else 1,
0 if candidate.estimate.validation.valid else 1,
0 if not candidate.estimate.validation.requires_approval else 1,
usage_price if preference == "lower_usage_price" else -ltv,
-ltv if preference == "lower_usage_price" else usage_price,
)
def _select_candidate(
candidates: tuple[_CandidateAssessment, ...],
preference: SolverPreference,
) -> _CandidateAssessment:
acceptable = _acceptable_candidates(candidates)
if acceptable:
return min(acceptable, key=lambda candidate: _candidate_sort_key(candidate, preference))
return min(candidates, key=lambda candidate: _fallback_sort_key(candidate, preference))
def solve_customer_tuning(
base_configuration: PricingConfiguration,
reference_configurations: list[PricingConfiguration],
profile: ComparableCustomerProfile,
boundary_policy: BoundaryPolicy,
ltv_policy: LTVPolicy,
request: CustomerTuningRequest,
search_policy: UsagePriceSearchPolicy | None = None,
) -> CustomerTuningOutcome:
if _usage_component(base_configuration) is None:
raise ValueError("customer tuning prototype currently requires a usage-priced model")
reference_estimates = [
estimate_comparable_customer_ltv(configuration, profile, boundary_policy, ltv_policy)
for configuration in reference_configurations
]
reference_estimate = select_reference_estimate(reference_estimates, profile.eligible_model_ids)
threshold = (
required_improvement_threshold(
reference_estimate.average_comparable_customer_lifetime_value,
ltv_policy.required_improvement_factor,
)
if reference_estimate is not None
else None
)
candidates: list[_CandidateAssessment] = []
for usage_unit_price in _price_range(
base_configuration,
_resolved_search_policy(base_configuration, request, search_policy),
):
configuration = _candidate_configuration(base_configuration, request, usage_unit_price)
estimate = estimate_comparable_customer_ltv(
configuration,
profile,
boundary_policy,
ltv_policy,
)
passes_required_improvement = (
True
if threshold is None
else estimate.average_comparable_customer_lifetime_value >= threshold
)
decision = _candidate_decision(
estimate.validation,
passes_required_improvement,
request.approval_mode,
)
candidates.append(
_CandidateAssessment(
configuration=configuration,
estimate=estimate,
decision=decision,
passes_required_improvement=passes_required_improvement,
improvement_vs_reference_pct=(
_percent_delta(
estimate.average_comparable_customer_lifetime_value,
reference_estimate.average_comparable_customer_lifetime_value,
)
if reference_estimate is not None
else None
),
)
)
if not candidates:
raise ValueError("customer tuning search produced no candidates")
selected = _select_candidate(tuple(candidates), request.preference)
binding_constraints = _binding_constraints(selected.configuration, selected.estimate.validation)
tradeoffs = _tradeoffs(
base_configuration,
selected.configuration,
selected.estimate.validation,
)
explanation = _explanation(
selected,
request,
reference_estimate,
threshold,
tradeoffs,
binding_constraints,
)
return CustomerTuningOutcome(
model_id=base_configuration.model.id,
model_name=base_configuration.model.name,
decision=selected.decision,
valid=selected.estimate.validation.valid,
requires_approval=selected.estimate.validation.requires_approval,
preference=request.preference,
approval_mode=request.approval_mode,
request=request,
solved_configuration=selected.estimate.validation.configuration,
solved_usage_unit_price=selected.estimate.validation.metrics.usage_unit_price,
reference_model_id=reference_estimate.model_id if reference_estimate else None,
reference_model_name=reference_estimate.model_name if reference_estimate else None,
reference_ltv=(
reference_estimate.average_comparable_customer_lifetime_value
if reference_estimate is not None
else None
),
required_improvement_threshold=threshold,
average_comparable_customer_lifetime_value=(
selected.estimate.average_comparable_customer_lifetime_value
),
improvement_vs_reference_pct=selected.improvement_vs_reference_pct,
passes_required_improvement=selected.passes_required_improvement,
evaluated_candidates=len(candidates),
tradeoffs=tradeoffs,
binding_constraints=binding_constraints,
validation=selected.estimate.validation,
explanation=explanation,
)

View File

@@ -0,0 +1,174 @@
from __future__ import annotations
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any, Literal
GovernanceDecision = Literal["proceed", "approval_required", "blocked"]
RecommendationType = Literal["research", "simulation", "model_change", "execution"]
RecommendationPriority = Literal["high", "medium", "low"]
RiskSeverity = Literal["low", "medium", "high"]
HealthStatus = Literal["pass", "warn", "fail"]
def _decimal(value: Decimal | str | int | float | None, default: str = "0") -> Decimal:
if value in (None, ""):
return Decimal(default)
return Decimal(str(value))
@dataclass(frozen=True)
class GovernancePolicy:
policy_id: str
max_self_serve_discount_pct: Decimal = Decimal("10")
max_customer_visible_price_increase_pct: Decimal = Decimal("15")
max_active_experiments: int = 2
max_concurrent_candidate_rollouts: int = 1
require_approval_for_candidate_rollout: bool = True
require_approval_for_approximate_provider_mapping: bool = True
block_unsupported_provider_artifacts: bool = True
drift_blocks_execution: bool = True
require_approval_for_price_change: bool = True
require_customer_notice_for_price_increase: bool = True
customer_notice_days: int = 30
grandfather_existing_customers: bool = True
customer_visible_tuning_enabled: bool = False
customer_visible_tuning_requires_active_model: bool = True
communication_owner_role: str = "operator"
default_approver_role: str = "operator"
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ApprovalRequirement:
id: str
title: str
approver_role: str
reason: str
blocking: bool = True
@dataclass(frozen=True)
class GovernanceRisk:
id: str
severity: RiskSeverity
summary: str
mitigation: str
@dataclass(frozen=True)
class SupportingObservation:
id: str
title: str
summary: str
source_ref: str
value: str | None = None
@dataclass(frozen=True)
class GovernanceAssessment:
decision: GovernanceDecision
summary: str
approvals: tuple[ApprovalRequirement, ...]
risks: tuple[GovernanceRisk, ...]
supporting_observations: tuple[SupportingObservation, ...]
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class SellerRecommendation:
id: str
recommendation_type: RecommendationType
priority: RecommendationPriority
title: str
rationale: str
suggested_action: str
confidence: Decimal
governance: GovernanceAssessment
risks: tuple[GovernanceRisk, ...]
supporting_observations: tuple[SupportingObservation, ...]
related_model_ids: tuple[str, ...] = ()
related_profile_ids: tuple[str, ...] = ()
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class HealthCheck:
id: str
title: str
status: HealthStatus
summary: str
value: str | None = None
threshold: str | None = None
suggested_action: str | None = None
@dataclass(frozen=True)
class SafeTuningParameter:
key: str
label: str
description: str
data_type: str
default_value: str | None = None
min_value: str | None = None
max_value: str | None = None
customer_visible: bool = True
@dataclass(frozen=True)
class SafeTuningExample:
id: str
title: str
outcome: str
summary: str
customer_message: str
visible_to_customer: bool
tradeoffs: tuple[str, ...]
@dataclass(frozen=True)
class SafeTuningContract:
model_id: str
model_name: str
mode: str
customer_visible: bool
tunable_parameters: tuple[SafeTuningParameter, ...]
tradeoff_lexicon: dict[str, str]
examples: tuple[SafeTuningExample, ...]
notes: tuple[str, ...] = ()
def governance_policy_from_dict(raw: dict[str, Any]) -> GovernancePolicy:
return GovernancePolicy(
policy_id=raw.get("policy_id", "default-governance-policy"),
max_self_serve_discount_pct=_decimal(raw.get("max_self_serve_discount_pct"), "10"),
max_customer_visible_price_increase_pct=_decimal(
raw.get("max_customer_visible_price_increase_pct"),
"15",
),
max_active_experiments=int(raw.get("max_active_experiments", 2)),
max_concurrent_candidate_rollouts=int(raw.get("max_concurrent_candidate_rollouts", 1)),
require_approval_for_candidate_rollout=bool(
raw.get("require_approval_for_candidate_rollout", True)
),
require_approval_for_approximate_provider_mapping=bool(
raw.get("require_approval_for_approximate_provider_mapping", True)
),
block_unsupported_provider_artifacts=bool(
raw.get("block_unsupported_provider_artifacts", True)
),
drift_blocks_execution=bool(raw.get("drift_blocks_execution", True)),
require_approval_for_price_change=bool(raw.get("require_approval_for_price_change", True)),
require_customer_notice_for_price_increase=bool(
raw.get("require_customer_notice_for_price_increase", True)
),
customer_notice_days=int(raw.get("customer_notice_days", 30)),
grandfather_existing_customers=bool(raw.get("grandfather_existing_customers", True)),
customer_visible_tuning_enabled=bool(raw.get("customer_visible_tuning_enabled", False)),
customer_visible_tuning_requires_active_model=bool(
raw.get("customer_visible_tuning_requires_active_model", True)
),
communication_owner_role=raw.get("communication_owner_role", "operator"),
default_approver_role=raw.get("default_approver_role", "operator"),
metadata=dict(raw.get("metadata", {})),
)

View File

@@ -0,0 +1,323 @@
from __future__ import annotations
import json
from dataclasses import dataclass, field
from decimal import Decimal
from pathlib import Path
from typing import Any, Literal
PricingModelStatus = Literal["active", "candidate", "retired"]
ChargeComponentKind = Literal[
"access",
"setup",
"usage",
"support",
"discount",
"risk_adjustment",
]
ParameterClass = Literal[
"fixed",
"seller_controlled",
"customer_tunable",
"calculated",
"constrained",
"provider",
]
_ALLOWED_COMPONENT_KINDS = {
"access",
"setup",
"usage",
"support",
"discount",
"risk_adjustment",
}
_ALLOWED_PARAMETER_CLASSES = {
"fixed",
"seller_controlled",
"customer_tunable",
"calculated",
"constrained",
"provider",
}
def _money(value: str | int | float | Decimal | None) -> Decimal | None:
if value in (None, ""):
return None
return Decimal(str(value))
def _tuple_dict(value: dict[str, Any] | None) -> dict[str, Any]:
return dict(value or {})
@dataclass(frozen=True)
class ChargeComponent:
id: str
kind: ChargeComponentKind | str
amount: Decimal | None = None
cadence: str | None = None
meter: str | None = None
unit: str | None = None
unit_price: Decimal | None = None
included_units: Decimal | None = None
label: str | None = None
billing_treatment: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class Commitment:
id: str
kind: str
value: str
unit: str | None = None
description: str = ""
@dataclass(frozen=True)
class TunableParameter:
key: str
parameter_class: ParameterClass | str
data_type: str
description: str = ""
default_value: str | None = None
min_value: Decimal | None = None
max_value: Decimal | None = None
options: tuple[str, ...] = ()
@dataclass(frozen=True)
class PricingModel:
id: str
name: str
model_type: str
lifecycle_phase: str
currency: str
access_fee_amount: Decimal
access_fee_cadence: str
status: PricingModelStatus
description: str = ""
included_usage: str | None = None
overage_meter: str | None = None
charge_components: tuple[ChargeComponent, ...] = ()
commitments: tuple[Commitment, ...] = ()
tunable_parameters: tuple[TunableParameter, ...] = ()
eligibility: tuple[str, ...] = ()
provider_hints: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
def _read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def _parse_charge_component(raw: dict[str, Any]) -> ChargeComponent:
return ChargeComponent(
id=raw["id"],
kind=raw["kind"],
amount=_money(raw.get("amount")),
cadence=raw.get("cadence"),
meter=raw.get("meter"),
unit=raw.get("unit"),
unit_price=_money(raw.get("unit_price")),
included_units=_money(raw.get("included_units")),
label=raw.get("label"),
billing_treatment=raw.get("billing_treatment"),
metadata=_tuple_dict(raw.get("metadata")),
)
def _parse_commitment(raw: dict[str, Any]) -> Commitment:
return Commitment(
id=raw["id"],
kind=raw["kind"],
value=str(raw["value"]),
unit=raw.get("unit"),
description=raw.get("description", ""),
)
def _parse_tunable_parameter(raw: dict[str, Any]) -> TunableParameter:
return TunableParameter(
key=raw["key"],
parameter_class=raw["parameter_class"],
data_type=raw["data_type"],
description=raw.get("description", ""),
default_value=str(raw["default_value"]) if raw.get("default_value") is not None else None,
min_value=_money(raw.get("min_value")),
max_value=_money(raw.get("max_value")),
options=tuple(str(item) for item in raw.get("options", [])),
)
def _legacy_charge_components(raw: dict[str, Any]) -> list[dict[str, Any]]:
components: list[dict[str, Any]] = [
{
"id": f"{raw['id']}-access",
"kind": "access",
"amount": raw["access_fee_amount"],
"cadence": raw["access_fee_cadence"],
"label": "Recurring access fee",
"billing_treatment": "recurring",
"metadata": {"included_usage": raw.get("included_usage")},
}
]
if raw.get("model_type") == "hybrid_subscription_usage" or raw.get("overage_meter"):
components.append(
{
"id": f"{raw['id']}-usage",
"kind": "usage",
"meter": raw.get("overage_meter") or "usage",
"unit": raw.get("unit") or "usage_unit",
"included_units": raw.get("included_units") or raw.get("included_tokens"),
"unit_price": raw.get("unit_price") or raw.get("overage_unit_price"),
"label": "Variable usage component",
"billing_treatment": "metered",
"metadata": {"included_usage": raw.get("included_usage")},
}
)
return components
def _access_component(components: tuple[ChargeComponent, ...]) -> ChargeComponent | None:
return next((component for component in components if component.kind == "access"), None)
def _usage_component(components: tuple[ChargeComponent, ...]) -> ChargeComponent | None:
return next((component for component in components if component.kind == "usage"), None)
def _parse_pricing_model(raw: dict[str, Any]) -> PricingModel:
charge_components = tuple(
_parse_charge_component(item)
for item in (raw.get("charge_components") or _legacy_charge_components(raw))
)
access_component = _access_component(charge_components)
usage_component = _usage_component(charge_components)
access_fee_amount = _money(raw.get("access_fee_amount"))
if access_fee_amount is None:
access_fee_amount = access_component.amount if access_component else Decimal("0")
access_fee_cadence = raw.get("access_fee_cadence")
if access_fee_cadence is None:
access_fee_cadence = access_component.cadence if access_component else "monthly"
metadata = _tuple_dict(raw.get("metadata"))
included_usage = raw.get("included_usage") or metadata.get("included_usage")
if included_usage is None and access_component:
included_usage = access_component.metadata.get("included_usage")
if included_usage is None and usage_component:
included_usage = usage_component.metadata.get("included_usage")
overage_meter = raw.get("overage_meter") or (usage_component.meter if usage_component else None)
return PricingModel(
id=raw["id"],
name=raw["name"],
model_type=raw["model_type"],
lifecycle_phase=raw["lifecycle_phase"],
currency=raw["currency"],
access_fee_amount=access_fee_amount or Decimal("0"),
access_fee_cadence=access_fee_cadence or "monthly",
status=raw["status"],
description=raw.get("description", ""),
included_usage=included_usage,
overage_meter=overage_meter,
charge_components=charge_components,
commitments=tuple(
_parse_commitment(item) for item in raw.get("commitments", [])
),
tunable_parameters=tuple(
_parse_tunable_parameter(item) for item in raw.get("tunable_parameters", [])
),
eligibility=tuple(str(item) for item in raw.get("eligibility", [])),
provider_hints=_tuple_dict(raw.get("provider_hints")),
metadata=metadata,
)
def load_pricing_models(path: str | Path) -> list[PricingModel]:
raw = _read_json(Path(path))
models = [_parse_pricing_model(item) for item in raw["models"]]
issues = validate_pricing_catalog(models)
if issues:
formatted = "; ".join(
f"{model_id}: {', '.join(model_issues)}"
for model_id, model_issues in sorted(issues.items())
)
raise ValueError(f"invalid pricing catalog: {formatted}")
return models
def validate_pricing_catalog(models: list[PricingModel]) -> dict[str, list[str]]:
issues: dict[str, list[str]] = {}
ids = [model.id for model in models]
if len(ids) != len(set(ids)):
issues.setdefault("__catalog__", []).append("duplicate model ids")
for model in models:
model_issues = validate_pricing_model(model)
if model_issues:
issues[model.id] = model_issues
return issues
def validate_pricing_model(model: PricingModel) -> list[str]:
issues: list[str] = []
if model.status not in {"active", "candidate", "retired"}:
issues.append(f"unsupported status '{model.status}'")
if model.access_fee_amount < Decimal("0"):
issues.append("access_fee_amount must be non-negative")
if not model.charge_components:
issues.append("at least one charge component is required")
component_ids = [component.id for component in model.charge_components]
if len(component_ids) != len(set(component_ids)):
issues.append("charge component ids must be unique")
access_components = [component for component in model.charge_components if component.kind == "access"]
if len(access_components) != 1:
issues.append("exactly one access charge component is required")
for component in model.charge_components:
if component.kind not in _ALLOWED_COMPONENT_KINDS:
issues.append(f"unsupported charge component kind '{component.kind}'")
if component.kind == "access":
if component.amount is None:
issues.append("access charge component must define amount")
if component.cadence is None:
issues.append("access charge component must define cadence")
if component.kind == "usage" and component.meter is None:
issues.append("usage charge component must define meter")
usage_components = [component for component in model.charge_components if component.kind == "usage"]
if model.model_type == "hybrid_subscription_usage" and not usage_components:
issues.append("hybrid_subscription_usage requires a usage charge component")
tunable_keys = [parameter.key for parameter in model.tunable_parameters]
if len(tunable_keys) != len(set(tunable_keys)):
issues.append("tunable parameter keys must be unique")
for parameter in model.tunable_parameters:
if parameter.parameter_class not in _ALLOWED_PARAMETER_CLASSES:
issues.append(f"unsupported parameter_class '{parameter.parameter_class}'")
if (
parameter.parameter_class == "customer_tunable"
and not parameter.options
and parameter.min_value is None
and parameter.max_value is None
):
issues.append(
f"customer_tunable parameter '{parameter.key}' must define bounds or options"
)
commitment_ids = [commitment.id for commitment in model.commitments]
if len(commitment_ids) != len(set(commitment_ids)):
issues.append("commitment ids must be unique")
return issues

View File

@@ -0,0 +1,696 @@
from __future__ import annotations
import json
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any, Literal
from .pricing_models import PricingModel
PublicationArtifactKind = Literal["product", "meter", "price", "commitment", "configuration"]
ArtifactMappingStatus = Literal["exact", "approximate", "unsupported"]
PublicationOperationKind = Literal["create", "update", "noop", "retire", "rollback"]
DriftSeverity = Literal["info", "warn", "error"]
def _serialize_value(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {
key: _serialize_value(getattr(value, key))
for key in value.__dataclass_fields__
}
if isinstance(value, tuple):
return [_serialize_value(item) for item in value]
if isinstance(value, list):
return [_serialize_value(item) for item in value]
if isinstance(value, dict):
return {key: _serialize_value(item) for key, item in value.items()}
return value
def _payload_signature(payload: Any) -> str:
return json.dumps(_serialize_value(payload), sort_keys=True)
@dataclass(frozen=True)
class CatalogProduct:
id: str
name: str
description: str
currency: str
lifecycle_phase: str
active_pricing_model_id: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishableProduct:
key: str
product_id: str
name: str
description: str
currency: str
lifecycle_phase: str
active: bool
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishableMeter:
key: str
meter_id: str
name: str
event_name: str
unit: str
aggregation: str = "sum"
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishablePrice:
key: str
price_id: str
product_key: str
component_id: str
component_kind: str
label: str
currency: str
billing_treatment: str
cadence: str | None = None
amount: Decimal | None = None
unit_price: Decimal | None = None
included_units: Decimal | None = None
meter_key: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishableCommitment:
key: str
commitment_id: str
kind: str
value: str
unit: str | None = None
description: str = ""
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishableConfiguration:
key: str
configuration_id: str
product_key: str
model_id: str
model_name: str
segment: str | None
price_keys: tuple[str, ...]
commitment_keys: tuple[str, ...]
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublicationBundle:
bundle_id: str
model_id: str
model_name: str
product: PublishableProduct
meters: tuple[PublishableMeter, ...]
prices: tuple[PublishablePrice, ...]
commitments: tuple[PublishableCommitment, ...]
configurations: tuple[PublishableConfiguration, ...]
provider_hints: dict[str, Any] = field(default_factory=dict)
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class ProviderMappedArtifact:
provider: str
source_key: str
source_kind: PublicationArtifactKind
provider_id: str
provider_object_type: str
mapping_status: ArtifactMappingStatus
payload: dict[str, Any]
metadata: dict[str, Any] = field(default_factory=dict)
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class ProviderPublicationPackage:
provider: str
bundle_id: str
model_id: str
model_name: str
artifacts: tuple[ProviderMappedArtifact, ...]
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class DriftFinding:
provider_id: str
provider_object_type: str
severity: DriftSeverity
summary: str
expected: dict[str, Any] = field(default_factory=dict)
actual: dict[str, Any] = field(default_factory=dict)
suggested_action: str | None = None
@dataclass(frozen=True)
class PublicationOperation:
kind: PublicationOperationKind
provider_id: str
provider_object_type: str
source_key: str | None
source_kind: PublicationArtifactKind | None
mapping_status: ArtifactMappingStatus | None
summary: str
desired_payload: dict[str, Any] = field(default_factory=dict)
current_payload: dict[str, Any] = field(default_factory=dict)
desired_metadata: dict[str, Any] = field(default_factory=dict)
current_metadata: dict[str, Any] = field(default_factory=dict)
desired_notes: tuple[str, ...] = ()
current_notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class PublishedProviderArtifact:
provider: str
source_key: str
source_kind: PublicationArtifactKind
provider_id: str
provider_object_type: str
mapping_status: ArtifactMappingStatus
payload: dict[str, Any]
metadata: dict[str, Any] = field(default_factory=dict)
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class PublicationRevision:
revision_id: str
model_id: str
model_name: str
summary: str
operations: tuple[PublicationOperation, ...]
snapshot: tuple[PublishedProviderArtifact, ...]
replaced_revision_id: str | None = None
@dataclass(frozen=True)
class ProviderPublicationState:
provider: str
active_revision_id: str | None = None
active_model_id: str | None = None
artifacts: tuple[PublishedProviderArtifact, ...] = ()
revisions: tuple[PublicationRevision, ...] = ()
@dataclass(frozen=True)
class PublicationPlan:
provider: str
bundle_id: str
model_id: str
model_name: str
operations: tuple[PublicationOperation, ...]
drift: tuple[DriftFinding, ...]
unsupported_artifacts: tuple[ProviderMappedArtifact, ...]
summary: str
@dataclass(frozen=True)
class PublicationApplyResult:
plan: PublicationPlan
revision: PublicationRevision
state: ProviderPublicationState
summary: str
def build_publication_bundle(
product: CatalogProduct,
model: PricingModel,
*,
configuration_id: str | None = None,
segment: str | None = None,
) -> PublicationBundle:
product_key = f"product:{product.id}"
publishable_product = PublishableProduct(
key=product_key,
product_id=product.id,
name=product.name,
description=product.description,
currency=product.currency,
lifecycle_phase=product.lifecycle_phase,
active=model.status != "retired",
metadata={
**product.metadata,
"adaptive_pricing_model_id": model.id,
"adaptive_pricing_model_status": model.status,
},
)
meters: list[PublishableMeter] = []
prices: list[PublishablePrice] = []
for component in model.charge_components:
meter_key: str | None = None
if component.kind == "usage" and component.meter:
meter_key = f"meter:{model.id}:{component.id}"
meters.append(
PublishableMeter(
key=meter_key,
meter_id=component.meter,
name=component.label or component.meter,
event_name=component.meter,
unit=component.unit or "usage_unit",
metadata={
"adaptive_pricing_model_id": model.id,
"source_component_id": component.id,
},
)
)
prices.append(
PublishablePrice(
key=f"price:{model.id}:{component.id}",
price_id=f"{model.id}:{component.id}",
product_key=product_key,
component_id=component.id,
component_kind=component.kind,
label=component.label or component.id,
currency=model.currency,
billing_treatment=component.billing_treatment or "recurring",
cadence=component.cadence or (
model.access_fee_cadence if component.kind in {"access", "support", "discount", "risk_adjustment"} else None
),
amount=component.amount,
unit_price=component.unit_price,
included_units=component.included_units,
meter_key=meter_key,
metadata={
**component.metadata,
"adaptive_pricing_model_id": model.id,
"source_component_id": component.id,
"source_component_kind": component.kind,
},
)
)
commitments = tuple(
PublishableCommitment(
key=f"commitment:{model.id}:{commitment.id}",
commitment_id=commitment.id,
kind=commitment.kind,
value=commitment.value,
unit=commitment.unit,
description=commitment.description,
metadata={"adaptive_pricing_model_id": model.id},
)
for commitment in model.commitments
)
configuration = PublishableConfiguration(
key=f"configuration:{configuration_id or model.id}",
configuration_id=configuration_id or model.id,
product_key=product_key,
model_id=model.id,
model_name=model.name,
segment=segment,
price_keys=tuple(price.key for price in prices),
commitment_keys=tuple(commitment.key for commitment in commitments),
metadata={
"adaptive_pricing_model_id": model.id,
"lifecycle_phase": model.lifecycle_phase,
},
)
notes = (
"Publication bundles preserve the internal pricing model as the source of truth.",
"Provider mappings may mark artifacts exact, approximate, or unsupported without mutating the bundle.",
)
return PublicationBundle(
bundle_id=f"bundle:{model.id}",
model_id=model.id,
model_name=model.name,
product=publishable_product,
meters=tuple(meters),
prices=tuple(prices),
commitments=commitments,
configurations=(configuration,),
provider_hints=model.provider_hints,
notes=notes,
)
def _published_artifact(mapped: ProviderMappedArtifact) -> PublishedProviderArtifact:
return PublishedProviderArtifact(
provider=mapped.provider,
source_key=mapped.source_key,
source_kind=mapped.source_kind,
provider_id=mapped.provider_id,
provider_object_type=mapped.provider_object_type,
mapping_status=mapped.mapping_status,
payload=mapped.payload,
metadata=mapped.metadata,
notes=mapped.notes,
)
def _artifact_changed(
desired: ProviderMappedArtifact,
current: PublishedProviderArtifact,
) -> bool:
return any(
[
desired.mapping_status != current.mapping_status,
desired.provider_object_type != current.provider_object_type,
_payload_signature(desired.payload) != _payload_signature(current.payload),
_payload_signature(desired.metadata) != _payload_signature(current.metadata),
desired.notes != current.notes,
]
)
def _diff_summary(
desired: ProviderMappedArtifact,
current: PublishedProviderArtifact,
) -> DriftFinding:
return DriftFinding(
provider_id=desired.provider_id,
provider_object_type=desired.provider_object_type,
severity="warn",
summary="Provider shadow state differs from desired pricing definition.",
expected={
"payload": desired.payload,
"metadata": desired.metadata,
"mapping_status": desired.mapping_status,
"notes": list(desired.notes),
},
actual={
"payload": current.payload,
"metadata": current.metadata,
"mapping_status": current.mapping_status,
"notes": list(current.notes),
},
suggested_action="Publish the desired definition again or reconcile the provider-side drift.",
)
def plan_publication(
package: ProviderPublicationPackage,
current_state: ProviderPublicationState | None = None,
) -> PublicationPlan:
current_state = current_state or ProviderPublicationState(provider=package.provider)
current_index = {artifact.provider_id: artifact for artifact in current_state.artifacts}
desired_publishable = tuple(
artifact for artifact in package.artifacts if artifact.mapping_status != "unsupported"
)
unsupported = tuple(
artifact for artifact in package.artifacts if artifact.mapping_status == "unsupported"
)
operations: list[PublicationOperation] = []
drift: list[DriftFinding] = []
desired_ids = {artifact.provider_id for artifact in desired_publishable}
for artifact in desired_publishable:
current = current_index.get(artifact.provider_id)
if current is None:
operations.append(
PublicationOperation(
kind="create",
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
source_key=artifact.source_key,
source_kind=artifact.source_kind,
mapping_status=artifact.mapping_status,
summary="Create provider artifact from canonical pricing definition.",
desired_payload=artifact.payload,
desired_metadata=artifact.metadata,
desired_notes=artifact.notes,
)
)
continue
if _artifact_changed(artifact, current):
operations.append(
PublicationOperation(
kind="update",
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
source_key=artifact.source_key,
source_kind=artifact.source_kind,
mapping_status=artifact.mapping_status,
summary="Update provider artifact to match canonical pricing definition.",
desired_payload=artifact.payload,
current_payload=current.payload,
desired_metadata=artifact.metadata,
current_metadata=current.metadata,
desired_notes=artifact.notes,
current_notes=current.notes,
)
)
drift.append(_diff_summary(artifact, current))
continue
operations.append(
PublicationOperation(
kind="noop",
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
source_key=artifact.source_key,
source_kind=artifact.source_kind,
mapping_status=artifact.mapping_status,
summary="Provider artifact already matches canonical pricing definition.",
desired_payload=artifact.payload,
current_payload=current.payload,
desired_metadata=artifact.metadata,
current_metadata=current.metadata,
desired_notes=artifact.notes,
current_notes=current.notes,
)
)
for artifact in current_state.artifacts:
if artifact.provider_id in desired_ids:
continue
operations.append(
PublicationOperation(
kind="retire",
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
source_key=artifact.source_key,
source_kind=artifact.source_kind,
mapping_status=artifact.mapping_status,
summary="Retire managed provider artifact no longer present in the desired pricing definition.",
current_payload=artifact.payload,
current_metadata=artifact.metadata,
current_notes=artifact.notes,
)
)
drift.append(
DriftFinding(
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
severity="warn",
summary="Managed provider artifact exists in shadow state but not in the desired pricing definition.",
actual={
"payload": artifact.payload,
"metadata": artifact.metadata,
"mapping_status": artifact.mapping_status,
"notes": list(artifact.notes),
},
suggested_action="Retire the artifact or republish the desired model.",
)
)
summary = (
f"{package.provider}: {sum(op.kind == 'create' for op in operations)} create, "
f"{sum(op.kind == 'update' for op in operations)} update, "
f"{sum(op.kind == 'retire' for op in operations)} retire, "
f"{sum(op.kind == 'noop' for op in operations)} noop, "
f"{len(unsupported)} unsupported."
)
return PublicationPlan(
provider=package.provider,
bundle_id=package.bundle_id,
model_id=package.model_id,
model_name=package.model_name,
operations=tuple(operations),
drift=tuple(drift),
unsupported_artifacts=unsupported,
summary=summary,
)
def _next_revision_id(state: ProviderPublicationState) -> str:
return f"{state.provider}-rev-{len(state.revisions) + 1:04d}"
def _next_active_model_id(artifacts: list[PublishedProviderArtifact]) -> str | None:
product = next((artifact for artifact in artifacts if artifact.source_kind == "product"), None)
if product is None:
return None
if product.metadata.get("model_id"):
return str(product.metadata["model_id"])
payload_metadata = product.payload.get("metadata", {})
if payload_metadata.get("adaptive_pricing_model_id"):
return str(payload_metadata["adaptive_pricing_model_id"])
return None
def apply_publication(
package: ProviderPublicationPackage,
current_state: ProviderPublicationState | None = None,
) -> PublicationApplyResult:
current_state = current_state or ProviderPublicationState(provider=package.provider)
plan = plan_publication(package, current_state)
desired_index = {
artifact.provider_id: artifact
for artifact in package.artifacts
if artifact.mapping_status != "unsupported"
}
artifact_index = {artifact.provider_id: artifact for artifact in current_state.artifacts}
for operation in plan.operations:
if operation.kind in {"create", "update", "noop"}:
artifact_index[operation.provider_id] = _published_artifact(
desired_index[operation.provider_id]
)
elif operation.kind == "retire":
artifact_index.pop(operation.provider_id, None)
snapshot = tuple(sorted(artifact_index.values(), key=lambda artifact: artifact.provider_id))
revision = PublicationRevision(
revision_id=_next_revision_id(current_state),
model_id=package.model_id,
model_name=package.model_name,
summary=plan.summary,
operations=plan.operations,
snapshot=snapshot,
replaced_revision_id=current_state.active_revision_id,
)
state = ProviderPublicationState(
provider=package.provider,
active_revision_id=revision.revision_id,
active_model_id=_next_active_model_id(list(snapshot)),
artifacts=snapshot,
revisions=current_state.revisions + (revision,),
)
return PublicationApplyResult(
plan=plan,
revision=revision,
state=state,
summary=plan.summary,
)
def rollback_publication(
state: ProviderPublicationState,
revision_id: str,
) -> PublicationApplyResult:
revision = next((item for item in state.revisions if item.revision_id == revision_id), None)
if revision is None:
raise ValueError(f"unknown revision_id '{revision_id}'")
rollback_operation = PublicationOperation(
kind="rollback",
provider_id=revision.revision_id,
provider_object_type="revision",
source_key=None,
source_kind=None,
mapping_status=None,
summary=f"Rollback provider shadow state to {revision.revision_id}.",
)
new_revision = PublicationRevision(
revision_id=_next_revision_id(state),
model_id=revision.model_id,
model_name=revision.model_name,
summary=f"Rolled back provider shadow state to {revision.revision_id}.",
operations=(rollback_operation,),
snapshot=revision.snapshot,
replaced_revision_id=state.active_revision_id,
)
new_state = ProviderPublicationState(
provider=state.provider,
active_revision_id=new_revision.revision_id,
active_model_id=revision.model_id,
artifacts=revision.snapshot,
revisions=state.revisions + (new_revision,),
)
return PublicationApplyResult(
plan=PublicationPlan(
provider=state.provider,
bundle_id=f"rollback:{revision.revision_id}",
model_id=revision.model_id,
model_name=revision.model_name,
operations=(rollback_operation,),
drift=(),
unsupported_artifacts=(),
summary=new_revision.summary,
),
revision=new_revision,
state=new_state,
summary=new_revision.summary,
)
def provider_state_to_dict(state: ProviderPublicationState) -> dict[str, Any]:
return _serialize_value(state)
def _operation_from_dict(raw: dict[str, Any]) -> PublicationOperation:
return PublicationOperation(
kind=raw["kind"],
provider_id=raw["provider_id"],
provider_object_type=raw["provider_object_type"],
source_key=raw.get("source_key"),
source_kind=raw.get("source_kind"),
mapping_status=raw.get("mapping_status"),
summary=raw["summary"],
desired_payload=dict(raw.get("desired_payload", {})),
current_payload=dict(raw.get("current_payload", {})),
desired_metadata=dict(raw.get("desired_metadata", {})),
current_metadata=dict(raw.get("current_metadata", {})),
desired_notes=tuple(raw.get("desired_notes", [])),
current_notes=tuple(raw.get("current_notes", [])),
)
def _artifact_from_dict(raw: dict[str, Any]) -> PublishedProviderArtifact:
return PublishedProviderArtifact(
provider=raw["provider"],
source_key=raw["source_key"],
source_kind=raw["source_kind"],
provider_id=raw["provider_id"],
provider_object_type=raw["provider_object_type"],
mapping_status=raw["mapping_status"],
payload=dict(raw.get("payload", {})),
metadata=dict(raw.get("metadata", {})),
notes=tuple(raw.get("notes", [])),
)
def _revision_from_dict(raw: dict[str, Any]) -> PublicationRevision:
return PublicationRevision(
revision_id=raw["revision_id"],
model_id=raw["model_id"],
model_name=raw["model_name"],
summary=raw["summary"],
operations=tuple(_operation_from_dict(item) for item in raw.get("operations", [])),
snapshot=tuple(_artifact_from_dict(item) for item in raw.get("snapshot", [])),
replaced_revision_id=raw.get("replaced_revision_id"),
)
def provider_state_from_dict(raw: dict[str, Any]) -> ProviderPublicationState:
return ProviderPublicationState(
provider=raw["provider"],
active_revision_id=raw.get("active_revision_id"),
active_model_id=raw.get("active_model_id"),
artifacts=tuple(_artifact_from_dict(item) for item in raw.get("artifacts", [])),
revisions=tuple(_revision_from_dict(item) for item in raw.get("revisions", [])),
)

View File

@@ -0,0 +1,330 @@
from __future__ import annotations
from typing import Any
from .provider_publication import (
ProviderMappedArtifact,
ProviderPublicationPackage,
PublicationBundle,
PublishableCommitment,
PublishableConfiguration,
PublishableMeter,
PublishablePrice,
)
def _lookup_key(*parts: str) -> str:
return "--".join(
part.replace(":", "-").replace("_", "-")
for part in parts
if part
)
def _stripe_interval(value: str | None) -> str | None:
if value is None:
return None
normalized = value.lower()
if normalized in {"monthly", "month"}:
return "month"
if normalized in {"yearly", "annual", "year"}:
return "year"
return normalized
def _stripe_hints(bundle: PublicationBundle) -> dict[str, Any]:
return dict(bundle.provider_hints.get("stripe", {}))
def _product_provider_id(bundle: PublicationBundle) -> str:
hints = _stripe_hints(bundle)
return hints.get("product_lookup_key") or _lookup_key("product", bundle.product.product_id)
def _meter_provider_id(bundle: PublicationBundle, meter: PublishableMeter) -> str:
hints = _stripe_hints(bundle)
if hints.get("meter_name") and len(bundle.meters) == 1:
return str(hints["meter_name"])
return _lookup_key("meter", bundle.model_id, meter.meter_id)
def _product_artifact(bundle: PublicationBundle) -> ProviderMappedArtifact:
hints = _stripe_hints(bundle)
return ProviderMappedArtifact(
provider="stripe",
source_key=bundle.product.key,
source_kind="product",
provider_id=_product_provider_id(bundle),
provider_object_type="product",
mapping_status="exact",
payload={
"lookup_key": _product_provider_id(bundle),
"name": bundle.product.name,
"description": bundle.product.description,
"active": bundle.product.active,
"metadata": {
**bundle.product.metadata,
"collection_method": hints.get("collection_method", "charge_automatically"),
"source_of_truth": "adaptive-pricing",
},
},
metadata={"model_id": bundle.model_id},
notes=("Stripe product mapping is direct for catalog identity and metadata.",),
)
def _meter_artifact(bundle: PublicationBundle, meter: PublishableMeter) -> ProviderMappedArtifact:
provider_id = _meter_provider_id(bundle, meter)
return ProviderMappedArtifact(
provider="stripe",
source_key=meter.key,
source_kind="meter",
provider_id=provider_id,
provider_object_type="billing_meter",
mapping_status="exact",
payload={
"lookup_key": provider_id,
"display_name": meter.name,
"event_name": meter.event_name,
"default_aggregation": {"formula": meter.aggregation},
"unit_label": meter.unit,
"metadata": {
**meter.metadata,
"source_of_truth": "adaptive-pricing",
},
},
metadata={"model_id": bundle.model_id},
notes=("Stripe meter mapping is direct for metered usage identifiers.",),
)
def _fixed_price_payload(bundle: PublicationBundle, price: PublishablePrice) -> dict[str, Any]:
payload = {
"lookup_key": _lookup_key("price", bundle.model_id, price.component_id),
"product": _product_provider_id(bundle),
"currency": price.currency.lower(),
"nickname": price.label,
"unit_amount_decimal": str(price.amount or "0"),
"metadata": {
**price.metadata,
"source_of_truth": "adaptive-pricing",
},
}
if price.billing_treatment != "one_time" and price.cadence:
payload["recurring"] = {"interval": _stripe_interval(price.cadence)}
return payload
def _discount_artifact(bundle: PublicationBundle, price: PublishablePrice) -> ProviderMappedArtifact:
provider_id = _lookup_key("coupon", bundle.model_id, price.component_id)
return ProviderMappedArtifact(
provider="stripe",
source_key=price.key,
source_kind="price",
provider_id=provider_id,
provider_object_type="coupon",
mapping_status="approximate",
payload={
"lookup_key": provider_id,
"amount_off_decimal": str(abs(price.amount or 0)),
"currency": price.currency.lower(),
"name": price.label,
"metadata": {
**price.metadata,
"source_of_truth": "adaptive-pricing",
},
},
metadata={"model_id": bundle.model_id},
notes=(
"Stripe coupons approximate discount components because attachment to subscriptions and eligibility rules lives outside the price object.",
),
)
def _usage_price_artifact(
bundle: PublicationBundle,
price: PublishablePrice,
) -> ProviderMappedArtifact:
provider_id = _lookup_key("price", bundle.model_id, price.component_id)
if price.meter_key is None or price.unit_price is None:
return ProviderMappedArtifact(
provider="stripe",
source_key=price.key,
source_kind="price",
provider_id=provider_id,
provider_object_type="price",
mapping_status="unsupported",
payload={
"lookup_key": provider_id,
"reason": "usage component lacks a billable per-unit price or mapped meter",
},
metadata={"model_id": bundle.model_id},
notes=(
"Stripe publication cannot create a metered price without both a meter and a per-unit charge.",
),
)
meter_provider_id = _lookup_key("meter", bundle.model_id, price.component_id)
if len(bundle.meters) == 1:
meter_provider_id = _meter_provider_id(bundle, bundle.meters[0])
status = "approximate" if price.included_units not in (None, 0) else "exact"
notes = [
"Stripe metered price mapping is direct for per-unit overage billing.",
]
if status == "approximate":
notes.append(
"Included usage allowance requires supplemental credits, invoice adjustments, or custom entitlement logic outside the Stripe price object."
)
return ProviderMappedArtifact(
provider="stripe",
source_key=price.key,
source_kind="price",
provider_id=provider_id,
provider_object_type="price",
mapping_status=status,
payload={
"lookup_key": provider_id,
"product": _product_provider_id(bundle),
"currency": price.currency.lower(),
"nickname": price.label,
"billing_scheme": "per_unit",
"unit_amount_decimal": str(price.unit_price),
"recurring": {
"interval": _stripe_interval(price.cadence) or "month",
"usage_type": "metered",
},
"meter": meter_provider_id,
"metadata": {
**price.metadata,
"included_units": str(price.included_units) if price.included_units is not None else None,
"source_of_truth": "adaptive-pricing",
},
},
metadata={"model_id": bundle.model_id},
notes=tuple(notes),
)
def _price_artifact(bundle: PublicationBundle, price: PublishablePrice) -> ProviderMappedArtifact:
provider_id = _lookup_key("price", bundle.model_id, price.component_id)
if price.component_kind == "usage":
return _usage_price_artifact(bundle, price)
if price.component_kind == "discount":
return _discount_artifact(bundle, price)
return ProviderMappedArtifact(
provider="stripe",
source_key=price.key,
source_kind="price",
provider_id=provider_id,
provider_object_type="price",
mapping_status="exact",
payload=_fixed_price_payload(bundle, price),
metadata={"model_id": bundle.model_id},
notes=("Stripe price mapping is direct for fixed recurring or one-time charges.",),
)
def _commitment_artifact(
bundle: PublicationBundle,
commitment: PublishableCommitment,
) -> ProviderMappedArtifact:
provider_id = _lookup_key("commitment", bundle.model_id, commitment.commitment_id)
if commitment.kind == "contract_duration":
return ProviderMappedArtifact(
provider="stripe",
source_key=commitment.key,
source_kind="commitment",
provider_id=provider_id,
provider_object_type="metadata_binding",
mapping_status="approximate",
payload={
"lookup_key": provider_id,
"metadata": {
**commitment.metadata,
"contract_duration_value": commitment.value,
"contract_duration_unit": commitment.unit,
},
},
metadata={"model_id": bundle.model_id},
notes=(
"Stripe can store contract duration metadata, but enforcement still relies on subscription schedule policy or external contract workflow.",
),
)
return ProviderMappedArtifact(
provider="stripe",
source_key=commitment.key,
source_kind="commitment",
provider_id=provider_id,
provider_object_type="metadata_binding",
mapping_status="unsupported",
payload={
"lookup_key": provider_id,
"kind": commitment.kind,
"value": commitment.value,
"unit": commitment.unit,
},
metadata={"model_id": bundle.model_id},
notes=(
"Stripe metadata alone cannot enforce this commitment semantics; it remains an internal pricing and contract artifact.",
),
)
def _configuration_artifact(
bundle: PublicationBundle,
configuration: PublishableConfiguration,
) -> ProviderMappedArtifact:
provider_id = _lookup_key("configuration", configuration.configuration_id)
return ProviderMappedArtifact(
provider="stripe",
source_key=configuration.key,
source_kind="configuration",
provider_id=provider_id,
provider_object_type="metadata_binding",
mapping_status="approximate",
payload={
"lookup_key": provider_id,
"metadata": {
**configuration.metadata,
"configuration_id": configuration.configuration_id,
"segment": configuration.segment,
"price_keys": list(configuration.price_keys),
"commitment_keys": list(configuration.commitment_keys),
},
},
metadata={"model_id": bundle.model_id},
notes=(
"Customer or default pricing configurations can be recorded in Stripe metadata, but they are not first-class Stripe catalog objects.",
),
)
def map_bundle_to_stripe(bundle: PublicationBundle) -> ProviderPublicationPackage:
artifacts: list[ProviderMappedArtifact] = [_product_artifact(bundle)]
artifacts.extend(_meter_artifact(bundle, meter) for meter in bundle.meters)
artifacts.extend(_price_artifact(bundle, price) for price in bundle.prices)
artifacts.extend(_commitment_artifact(bundle, commitment) for commitment in bundle.commitments)
artifacts.extend(
_configuration_artifact(bundle, configuration)
for configuration in bundle.configurations
)
notes = [
"Stripe remains an execution backend; adaptive-pricing stays the source of truth.",
"Exact mappings create publishable Stripe shadow artifacts; approximate mappings require supplemental operational logic.",
]
if _stripe_hints(bundle).get("metered_usage_strategy") == "future_adapter":
notes.append(
"The pricing model declares a future metered-usage strategy hint, so Stripe publication keeps the usage allowance semantics descriptive rather than executable."
)
return ProviderPublicationPackage(
provider="stripe",
bundle_id=bundle.bundle_id,
model_id=bundle.model_id,
model_name=bundle.model_name,
artifacts=tuple(artifacts),
notes=tuple(notes),
)

View File

@@ -0,0 +1,78 @@
# Boundary Validation
Status: implementation-facing MVP for `ADAPTIVE-WP-0004`.
## Purpose
This document describes the first explicit boundary engine now available in
`adaptive_pricing_core.boundary_engine`.
The engine turns pricing-policy intent into inspectable validation outcomes
instead of leaving viability checks implicit in dashboard review.
## Inputs
The validator accepts:
- a canonical `PricingModel`
- a `PricingConfiguration` describing expected usage, fee assumptions, cost
allocation, optional price overrides, and commitment terms
- a `BoundaryPolicy` defining hard and soft limits
## Constraint Types
Current MVP constraints cover:
- segment eligibility
- expected usage variance limit
- payment fee ceiling
- cost-floor coverage
- minimum margin
- target-margin approval threshold
- discount exposure ceiling
- discount approval threshold
- commitment-backed concession enforcement
Hard constraints reject a configuration. Soft constraints mark it as valid only
with approval.
## Commitment Logic
The engine treats a concession as any configuration that weakens seller
economics relative to the model baseline under the same scenario assumptions.
A concession is considered meaningfully backed only when at least one of these
protections is present:
- minimum monthly turnover at or above modeled monthly revenue
- prepayment covering at least one modeled month
- guaranteed platform fee at or above modeled monthly revenue
- customer-funded onboarding that neutralizes onboarding cost
- materially longer contract duration
- reduced cancellation flexibility
## Outputs
Validation returns a `ValidationResult` with:
- `decision`: `accepted`, `requires_approval`, or `rejected`
- `valid` and `requires_approval`
- a human-readable summary
- machine-readable configuration snapshot
- machine-readable economics metrics
- per-constraint results with reasons, thresholds, and suggested actions
## Coulomb Adapter
The Coulomb observatory exposes this engine through
`observatory.boundary.build_boundary_validation()`.
That adapter currently uses:
- observed per-period payment fee rate
- observed AI usage cost
- observed per-member infrastructure cost allocation
- conservative default policy thresholds
This is intentionally an MVP policy surface. Later milestones can replace these
defaults with seller-managed governance data and richer LTV-aware constraints.

View File

@@ -0,0 +1,99 @@
# Comparable Customer LTV
Status: implementation-facing MVP for `ADAPTIVE-WP-0005`.
## Purpose
This document defines the first operational form of
`average_comparable_customer_lifetime_value`.
The goal is to compare pricing configurations using expected seller economics
over time instead of only the current-period observatory snapshot.
## Core Definition
For the current MVP:
`average_comparable_customer_lifetime_value`
means discounted expected seller margin for a comparable customer profile over a
finite horizon, minus acquisition and upfront seller investment costs.
Inputs include:
- validated monthly pricing economics from the boundary engine
- comparable-customer usage expectations
- churn and default risk assumptions
- contract duration and commitment protections
- seller acquisition and upfront investment costs
- a seller-configurable discount rate and required-improvement factor
## Reference Model Selection
The comparison engine selects:
`most_favorable_predefined_model`
as the highest-LTV valid predefined model available to the comparable-customer
profile. If no valid model exists, it falls back to the highest-LTV eligible
predefined model so the comparison still produces an inspectable anchor.
Eligibility is currently supplied by the simulation profile rather than derived
solely from model metadata.
## Required Improvement Semantics
For non-reference configurations:
```text
average_comparable_customer_lifetime_value(candidate)
>= average_comparable_customer_lifetime_value(reference)
× required_improvement_factor
```
When the reference LTV is positive, the threshold is multiplicative.
When the reference LTV is negative, the engine switches to additive improvement
semantics so the candidate must become less negative by the requested
percentage. This avoids the invalid outcome where multiplying a negative value
would reward a worse configuration.
## Risk Model
Current risk handling is intentionally simple and explicit:
- monthly churn risk applies after committed months expire
- monthly default risk applies throughout the horizon
- prepayment and guaranteed-fee commitments reduce default exposure
- reduced cancellation flexibility lowers modeled churn exposure
This is a policy approximation, not a retention model trained from history.
## Sensitivity Model
Each comparison runs the base case plus named sensitivity cases. The current
Coulomb adapter includes:
- usage downside
- usage upside
- risk spike
Sensitivity output reports:
- scenario LTV
- delta versus base LTV
- whether the configuration remains accepted, approval-only, or rejected
## Coulomb Calibration
The Coulomb observatory currently calibrates the generic engine with:
- observed payment-fee rate from `payment_records.json`
- observed AI usage unit cost from `usage_records.json`
- segment profiles from `ltv_scenarios.json`
- profile-specific fixed-cost allocation overrides for comparable future
customers
Those fixed-cost overrides are deliberate: the current single-member pilot cost
structure is too distorted to act as a reusable comparable-customer baseline on
its own.

View File

@@ -0,0 +1,93 @@
# Customer-Tuning Solver Prototype
Status: MVP for `ADAPTIVE-WP-0006`.
## Purpose
This milestone adds the first executable customer-tuning flow described in
`INTENT.md`.
The solver now accepts selected customer-tunable inputs, solves the remaining
usage-price parameter, validates the tuned configuration against boundary
constraints, and checks seller-side comparable-customer LTV against the best
available predefined reference model.
## Generic Solver Contract
Core module: `adaptive_pricing_core/customer_tuning.py`
Inputs:
- a baseline `PricingConfiguration`
- the comparable-customer profile
- boundary policy
- LTV policy
- a `CustomerTuningRequest`
- the set of predefined reference configurations available to that profile
Current request fields:
- `included_units`
- `contract_duration_months`
- `minimum_monthly_turnover`
- `prepaid_amount`
- `guaranteed_platform_fee`
- `customer_funded_onboarding`
- `reduced_cancellation_flexibility`
- preference: `lower_usage_price` or `seller_ltv`
- approval mode: `self_serve_only` or `allow_approval`
Current solved field:
- `usage_unit_price`
## Decision Logic
For each candidate usage price in the search range, the solver:
1. builds a tuned `PricingConfiguration`
2. runs boundary validation
3. estimates `average_comparable_customer_lifetime_value`
4. compares the tuned result with the best predefined reference model for the
profile
A tuned configuration is only accepted when:
- boundary validation is valid
- no seller approval is required when the request is `self_serve_only`
- tuned comparable-customer LTV meets the configured improvement threshold
The solver returns structured output including:
- accepted / rejected / requires approval decision
- solved configuration
- reference model and required LTV threshold
- binding constraints
- chosen trade-offs
- explanation text
## Coulomb Pilot
Pilot module: `projects/coulomb-pricing/observatory/tuning.py`
Pilot request catalog:
- `projects/coulomb-pricing/data/tuning_requests.json`
The Coulomb pilot currently targets `membership-plus-overage` against the
`small-team` comparable-customer profile.
Two pilot requests are shipped:
- a seller-safe lower-usage-price request that succeeds
- a high-included-usage request that is rejected for self-serve
## Current Modeling Note
The observatory simulation path still scales default hybrid included usage by
`members_per_customer`.
The tuning pilot interprets request-level `included_tokens` values as total
package allowances, then maps them into canonical configuration fields before
running the solver. This keeps the prototype aligned with the catalogs tunable
bounds while avoiding a broader simulation recalibration inside this milestone.

View File

@@ -0,0 +1,85 @@
# Governance Workflows
Status: MVP for `ADAPTIVE-WP-0008`.
## Purpose
This milestone turns pricing outputs into governed workflows instead of
standalone metrics.
The repository now exposes:
- a governance policy model
- governed seller recommendations
- a customer-facing safe-tuning contract surface
- pricing health checks
- provider-publication audit and revision surfaces
## Core And Adapter Layers
Generic core:
- `adaptive_pricing_core/governance.py`
Coulomb adapter:
- `projects/coulomb-pricing/observatory/governance.py`
- `projects/coulomb-pricing/data/governance_policy.json`
## Governance Policy
The policy model covers:
- approval thresholds
- customer-visible price-change rules
- experiment capacity
- candidate rollout limits
- provider execution limits
- customer communication ownership
- grandfathering and notice expectations
- customer-visible tuning enablement
For Coulomb, the current policy keeps customer-visible tuning disabled and
requires approval for candidate rollouts and approximate Stripe mappings.
## Recommendation Workflow
Recommendations now include:
- recommendation type: research, simulation, model change, or execution
- rationale
- confidence
- risks
- supporting observations
- governance decision
- approval requirements
This satisfies the PRD requirement that recommendations be explainable and
distinguish between evidence gathering, simulation, model design, and execution.
## Safe-Tuning Contract
The governance surface exposes a structured contract for customer-tunable
pricing:
- allowed tunable parameters
- a trade-off lexicon
- pilot examples
- whether a model is customer-visible or still pilot-only
For the current Coulomb MVP, the contract exists only as a pilot surface for
`membership-plus-overage`; accepted examples are still seller-assisted rather
than self-serve.
## Health And Audit
The dashboard payload now includes:
- pricing health checks
- provider execution readiness checks
- tuning pilot health
- experiment capacity checks
- provider revision history and active revision metadata
These surfaces are intended to help both humans and agents decide whether the
next safe step is research, simulation, approval, execution, or rollback.

View File

@@ -0,0 +1,113 @@
# Adaptive Pricing Implementation Roadmap
Status: draft, implementation-facing.
## Purpose
This roadmap translates the strategic goals in `INTENT.md` into an executable
implementation sequence for the current repository state.
The root research roadmap in `research/PricingResearchRoadmap.md` remains the
conceptual and research-first planning artifact. This document starts from the
current implementation reality: a Coulomb-specific economic observatory MVP
under `projects/coulomb-pricing/observatory/`.
## Current Baseline
The repository already provides:
- ledger-backed economics and liquidity tracking
- cost-floor, value-range, and market-context views
- file-based Stripe / Bubble / OpenRouter imports
- scenario comparison for a small set of candidate pricing models
- rules-based pricing recommendations
- a local dashboard API and UI
The repository does not yet provide:
- a canonical, generic pricing-model schema
- a validation engine for pricing constraints and commitments
- comparable customer LTV estimation
- a customer-tuning solver
- outbound payment-provider execution
- governance workflows for publishing and changing pricing
## Sequencing Principles
- Preserve the observatory MVP as a proving ground while extracting generic core logic.
- Build schema and validation before solver or provider execution.
- Keep the internal pricing model as the source of truth; execution adapters come later.
- Require explainability at each stage: validation, simulation, tuning, and publish.
- Convert repository planning into milestone workplans rather than one large umbrella plan.
## Milestones
| Milestone | Goal | Primary workplan | Dependencies | Exit signal |
| --- | --- | --- | --- | --- |
| M0 | Economic observatory baseline | `ADAPTIVE-WP-0002` | done | Coulomb ledger, dashboard, simulator, recommendations |
| M1 | Extract canonical pricing core and schema | `ADAPTIVE-WP-0003` | M0 | Generic schema and validator adopted by Coulomb data |
| M2 | Add boundary engine and explainable validation | `ADAPTIVE-WP-0004` | M1 | Pricing configurations are validated against explicit constraints |
| M3 | Upgrade economics to comparable-customer LTV and richer simulation | `ADAPTIVE-WP-0005` | M1, M2 | Simulations compare models using segment/risk-aware economics |
| M4 | Implement customer-tuning solver prototype | `ADAPTIVE-WP-0006` | M2, M3 | Tuned configurations can be proposed, evaluated, and explained |
| M5 | Add provider abstraction and Stripe publication flow | `ADAPTIVE-WP-0007` | M1, M2, M3 | Internal pricing definitions can publish Stripe artifacts safely |
| M6 | Add governance and recommendation workflows | `ADAPTIVE-WP-0008` | M4, M5 | Pricing changes become auditable, explainable, and operational |
## Milestone Details
### M1 — Canonical Pricing Core And Schema
Create a reusable pricing core that can represent more than the current
subscription-only observatory model. Expand beyond the current narrow
`PricingModel` structure to include charge components, commitments, tunable
parameters, eligibility, and provider-mapping metadata.
### M2 — Boundary Engine And Explainable Validation
Turn strategic boundary conditions into testable rules. A pricing configuration
must be accepted or rejected by explicit logic, not only by dashboard review.
This milestone defines hard and soft constraints, invalid-configuration reasons,
and commitment-backed discount semantics.
### M3 — Comparable Customer LTV And Richer Simulation
Extend the current period snapshot and fixed-assumption simulator into an engine
that can compare pricing configurations across customer segments, usage
forecasts, risk classes, and contract conditions. This is the economics core
needed by any seller-safe adaptive system.
### M4 — Customer-Tuning Solver Prototype
Expose a customer/seller configuration interface where selected parameters are
tunable and the solver adjusts the rest while preserving seller economics. This
milestone is the first actual realization of the adaptive-pricing thesis in
`INTENT.md`.
### M5 — Provider Abstraction And Stripe Publication
Add an outbound execution layer that maps internal pricing definitions to Stripe
artifacts. This is where the repo moves from observatory-only to operational
pricing infrastructure.
### M6 — Governance And Recommendation Workflows
Operationalize the engine with approval policies, publish/change workflows,
auditable recommendations, experiment guardrails, and customer-facing safe
tuning contracts.
## Workplan Set
- `ADAPTIVE-WP-0003` — Canonical pricing core and schema extraction
- `ADAPTIVE-WP-0004` — Boundary engine and explainable validation
- `ADAPTIVE-WP-0005` — Comparable customer LTV and simulation upgrade
- `ADAPTIVE-WP-0006` — Customer-tuning solver prototype
- `ADAPTIVE-WP-0007` — Provider abstraction and Stripe publication
- `ADAPTIVE-WP-0008` — Governance and recommendation workflows
## Near-Term Recommendation
Start with `ADAPTIVE-WP-0003`.
The current implementation bottleneck is not UI or provider execution. It is
the absence of a sufficiently expressive internal pricing model. Until that
schema exists, later milestones would hard-code MVP assumptions into layers that
should remain generic.

113
docs/PricingModelSchema.md Normal file
View File

@@ -0,0 +1,113 @@
# Pricing Model Schema
Status: draft, implementation-facing.
## Purpose
This document defines the canonical pricing-model schema now used by the
repository runtime. It is the implementation companion to the conceptual
vocabulary in `research/PricingOntology.md`.
The schema is designed to:
- preserve compatibility with the Coulomb observatory MVP
- represent richer pricing structures than a single subscription amount
- support later validation, solver, and provider-publication milestones
## Model Shape
Each pricing model contains:
- identity and lifecycle metadata
- normalized recurring access-fee fields for compatibility
- explicit charge components
- commitments
- tunable parameters
- eligibility and provider hints
- free-form metadata for deployment-specific details
## Canonical Fields
```yaml
id: string
name: string
model_type: flat_subscription | hybrid_subscription_usage | ...
lifecycle_phase: exploration | introduction | growth | maturity | saturation | decline
currency: EUR | USD | ...
status: active | candidate | retired
description: string
# Compatibility fields derived from the access component when omitted
access_fee_amount: decimal
access_fee_cadence: monthly | annual | one_time | ...
included_usage: string | null
overage_meter: string | null
charge_components:
- id: string
kind: access | setup | usage | support | discount | risk_adjustment
amount: decimal | null
cadence: string | null
meter: string | null
unit: string | null
unit_price: decimal | null
included_units: decimal | null
label: string | null
billing_treatment: recurring | metered | included | one_time | ...
metadata: {}
commitments:
- id: string
kind: minimum_turnover | contract_duration | prepayment | committed_usage | ...
value: string
unit: string | null
description: string
tunable_parameters:
- key: string
parameter_class: fixed | seller_controlled | customer_tunable | calculated | constrained | provider
data_type: string
description: string
default_value: string | null
min_value: decimal | null
max_value: decimal | null
options: []
eligibility:
- string
provider_hints: {}
metadata: {}
```
## Parameter Classes
- `fixed`: immutable in the selected model
- `seller_controlled`: adjustable only by the seller or internal workflow
- `customer_tunable`: intended to become solver-visible customer choice
- `calculated`: derived from other fields or economics
- `constrained`: externally set but bounded by validation rules
- `provider`: implementation-only parameter for execution backends
## Validation Rules
Current runtime validation enforces:
- model ids are unique
- charge component ids are unique within a model
- exactly one `access` charge component exists
- access components define amount and cadence
- usage components define a meter
- `hybrid_subscription_usage` models include a usage charge component
- tunable parameter keys are unique
- `customer_tunable` parameters declare bounds or enumerated options
- commitment ids are unique
## Transitional Compatibility
The Coulomb observatory still consumes `access_fee_amount`, `access_fee_cadence`,
`included_usage`, and `overage_meter`. The canonical loader back-fills these
from `charge_components` when the explicit top-level fields are omitted.
This keeps the current observatory stable while later milestones replace
hard-coded observatory assumptions with generic pricing-core behavior.

114
docs/StripePublication.md Normal file
View File

@@ -0,0 +1,114 @@
# Stripe Publication
Status: MVP for `ADAPTIVE-WP-0007`.
## Purpose
This milestone adds the first outbound execution layer for pricing models.
The implementation keeps `adaptive-pricing` as the source of truth and treats
Stripe as an execution backend. In this repository, publication targets a
file-backed Stripe shadow state rather than the live Stripe API.
## Core Modules
- `adaptive_pricing_core/provider_publication.py`
- `adaptive_pricing_core/stripe_provider.py`
The provider-publication core defines:
- provider-neutral publishable artifacts
- publication plans and operations
- drift findings
- revisioned shadow state
- rollback mechanics
The Stripe mapper translates publishable artifacts into Stripe-oriented objects
and marks each mapping as:
- `exact`
- `approximate`
- `unsupported`
## Publishable Artifact Model
Current provider-neutral artifacts:
- product
- meter
- price
- commitment
- configuration
Current Stripe-oriented object types:
- `product`
- `billing_meter`
- `price`
- `coupon`
- `metadata_binding`
`metadata_binding` is used for execution-adjacent information that Stripe can
store as metadata but does not treat as a first-class pricing object.
## Mapping Semantics
Current exact mappings:
- catalog product identity and metadata
- fixed recurring and one-time prices
- metered usage prices without bundled allowance semantics
- Stripe meter definitions
Current approximate mappings:
- metered prices that also imply included usage
- discount components mapped as coupon-like artifacts
- contract-duration commitments carried as metadata or schedule-adjacent data
- configuration artifacts carried as metadata
Current unsupported mappings:
- included-usage-only components without a billable per-unit overage price
- commitment semantics such as prepayment or minimum turnover when Stripe alone
cannot enforce them
## Coulomb Adapter
Project adapter:
- `projects/coulomb-pricing/observatory/publication.py`
- `projects/coulomb-pricing/observatory/publish.py`
Default local shadow-state path:
- `projects/coulomb-pricing/data/provider_state/stripe-publication.json`
Preview:
```bash
cd projects/coulomb-pricing
python3 -m observatory.publish --model-id flat-899-eur-monthly
```
Apply to the local shadow state:
```bash
cd projects/coulomb-pricing
python3 -m observatory.publish --model-id flat-899-eur-monthly --apply
```
Rollback:
```bash
cd projects/coulomb-pricing
python3 -m observatory.publish --rollback stripe-rev-0001
```
## Current Scope Limit
This milestone does not call the live Stripe API.
It establishes the internal publication model, Stripe object mapping,
idempotent shadow-state synchronization, drift detection, and rollback path so
live API execution can be layered on without making Stripe the source of truth.

View File

@@ -0,0 +1,19 @@
.PHONY: help design test serve
.DEFAULT_GOAL := help
REF ?=
help: ## List available make targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "}; {printf " %-12s %s\n", $$1, $$2}'
design: ## Re-vendor whynot-design (optional: REF=v0.2.1 or commit SHA)
./scripts/sync-whynot-design.sh $(REF)
python3 -m pytest -q tests/test_ui_vendor.py
test: ## Run the full pytest suite
python3 -m pytest -q
serve: ## Start the Economic Observatory UI on :8765
python3 -m observatory.server

View File

@@ -2,11 +2,75 @@
Project-specific material for the Coulomb Social Economic Observatory MVP.
This directory holds implementation artifacts, integrations, and documentation that
apply to the Coulomb deployment only. Generic adaptive-pricing framework concepts
belong in the repository root (`INTENT.md`, `docs/`, `research/`, `registry/`).
Generic adaptive-pricing framework concepts belong in the repository root
(`INTENT.md`, `docs/`, `research/`, `registry/`). Execution tracking:
`workplans/archived/260622-ADAPTIVE-WP-0002-economic-observatory-mvp.md`
(finished 2026-06-22).
**Execution tracking:** `workplans/ADAPTIVE-WP-0002-economic-observatory-mvp.md`
Liquidity and cost requirements: `REQUIREMENTS.md`.
UI workflow (whynot-design): `docs/UI-WORKFLOW.md`.
**Strategic positioning:** Adaptive Pricing is the pricing capability layer for
Coulomb offerings and related product ecosystems.
## Economic Observatory
The `observatory/` package reads **expense and payment record ledgers** and
computes all totals programmatically (`ledger.py``economics.py`).
| Ledger | File |
|--------|------|
| Budget (€1,000 start) | `data/budget.json` |
| Expense records | `data/expense_records.json` |
| Payment records | `data/payment_records.json` |
| Product model | `data/product.json` |
| Pricing models | `data/pricing-models.json` |
| Membership | `data/membership.json` |
| AI usage | `data/usage_records.json` |
| Credit wallets | `data/credit_wallets.json` |
| Value range hypotheses | `data/value_range.json` |
| Market signals | `data/market_signals.json` |
**Current reality:** infrastructure from January 2025 — domains **€6.75/mo**,
coulombcore hosting **€13.99/mo** (from Jan 2025), railiance01 hosting
**€8.99/mo** (from Mar 2026). Member **tegwick** pays **€8.99/mo** (Stripe fee
**€0.44**, net payout **€8.55** to binky-hedgehog) from November 2025. Customer
cost-pass-through billing is not active.
### Commands
```bash
cd projects/coulomb-pricing
make test
make design # pull whynot-design tokens/CSS/components
make design REF=v0.2.1 # bump to a tag or commit
python3 -m observatory --period 2026-06
python3 -m observatory --period 2026-06 --output reports/economics-2026-06.md
make serve
```
Open **http://127.0.0.1:8765/** for the Economic Observatory UI (served from
`ui/`, data via `/api/dashboard`). The UI consumes **whynot-design** (Layer 1 CSS
+ Layer 2 `<wn-*>` components) from `ui/vendor/whynot-design/`. See
`docs/UI-WORKFLOW.md` for the implementation process.
### Importers (file-based sync)
```bash
python3 -m observatory.importers.bubble --input data/imports/bubble-export.sample.json
python3 -m observatory.importers.stripe --input data/imports/stripe-export.sample.json
python3 -m observatory.importers.openrouter --input data/imports/openrouter-export.sample.json
```
Sample exports live under `data/imports/`. Live API sync can replace these
file-based importers in a follow-on workplan.
### Stripe Publication (shadow state)
```bash
cd projects/coulomb-pricing
python3 -m observatory.publish --model-id flat-899-eur-monthly
python3 -m observatory.publish --model-id flat-899-eur-monthly --apply
python3 -m observatory.publish --rollback stripe-rev-0001
```
These commands preview, apply, and roll back the local Stripe shadow state used
by the provider-publication MVP. The live Stripe API is still outside this
milestone.

View File

@@ -0,0 +1,83 @@
# Coulomb MVP — Liquidity & Cost Requirements
## Context
Coulomb Social carries **operator infrastructure costs** (domains, virtual
servers, payment-processing fees, future OpenRouter usage) while **customer
cost-pass-through billing is not active yet**. Members pay a flat subscription; they are not billed for
underlying platform spend.
The Economic Observatory must make the resulting **liquidity burn** visible.
## Requirements
### LQ-001 — Expense record ledger (source of truth)
Capture every operator expense as an individual record in
`data/expense_records.json`. Fields: `period`, `vendor`, `amount`, `currency`,
`cost_class`, `source`.
**Never store hand-calculated monthly or cumulative totals in data files.**
All aggregations must be computed programmatically by `observatory/ledger.py`.
### LQ-002 — Payment record ledger
Capture member subscription payments in `data/payment_records.json` with gross,
fees, and net amounts per period. Payment-processing totals are summed from
`fees_amount` — not duplicated as expense records.
### LQ-003 — Budget tracking
Maintain an operator liquidity budget (initial: **€1,000**) and compute
remaining budget after cumulative **infrastructure** spend minus cumulative net
member payments received.
### LQ-004 — Liquidity position
Report whether the project is **burning**, **neutral**, or **generating**
liquidity each period:
- `period_net = net_member_payments - infrastructure_cost`
- `cumulative_net = sum(period_net)`
- `remaining_budget = initial_budget + cumulative_net`
**No double-counting:** payment-processing fees are deducted from net member
payments. They are summed separately for economics reporting but must **not** be
subtracted again in the liquidity formula.
- `total_platform_cost = infrastructure_cost + payment_processing_cost` (for
gross-margin economics vs gross revenue)
- `cumulative_total_platform_cost` is informational; liquidity burn uses
`cumulative_infrastructure_cost` only
### LQ-005 — No customer cost billing (MVP boundary)
Do not allocate platform costs to customer invoices in MVP. Cost attribution
(OpenRouter per member, usage overage) is observatory-only until a later phase
introduces customer-visible credits or usage billing.
### LQ-006 — Historical dashboard
Economics Dashboard must show:
- Current-period economics (revenue, platform cost, margin)
- Cumulative liquidity summary (budget, burn, remaining)
- Monthly history table computed from ledgers
### LQ-007 — Deterministic calculation engine
All economics and liquidity figures must be produced by the Python observatory
package (`ledger.py`, `economics.py`). LLM or manual arithmetic must not be the
source of aggregated totals.
## Data sources (current)
| Ledger | Path |
|--------|------|
| Budget | `data/budget.json` |
| Expense records | `data/expense_records.json` |
| Payment records | `data/payment_records.json` |
| Membership | `data/membership.json` |
Future sprints replace manual records with Bubble, Stripe, and OpenRouter imports
while preserving the same ledger schema and calculation rules.

View File

@@ -0,0 +1,7 @@
{
"version": 1,
"currency": "EUR",
"initial_budget": "1000.00",
"started": "2025-01",
"note": "Operator liquidity pool for Coulomb Social MVP platform spend before the offering is cash-flow positive."
}

View File

@@ -0,0 +1,11 @@
{
"version": 1,
"currency": "EUR",
"wallets": [
{
"member_id": "member-tegwick",
"monthly_allowance_eur": "2.00",
"note": "Observatory-only allowance for hybrid pricing experiments"
}
]
}

View File

@@ -0,0 +1,586 @@
{
"version": 2,
"records": [
{
"id": "exp-domain-social-2025-01",
"period": "2025-01",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-01",
"period": "2025-01",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-01",
"period": "2025-01",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-02",
"period": "2025-02",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-02",
"period": "2025-02",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-02",
"period": "2025-02",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-03",
"period": "2025-03",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-03",
"period": "2025-03",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-03",
"period": "2025-03",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-04",
"period": "2025-04",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-04",
"period": "2025-04",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-04",
"period": "2025-04",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-05",
"period": "2025-05",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-05",
"period": "2025-05",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-05",
"period": "2025-05",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-06",
"period": "2025-06",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-06",
"period": "2025-06",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-06",
"period": "2025-06",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-07",
"period": "2025-07",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-07",
"period": "2025-07",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-07",
"period": "2025-07",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-08",
"period": "2025-08",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-08",
"period": "2025-08",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-08",
"period": "2025-08",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-09",
"period": "2025-09",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-09",
"period": "2025-09",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-09",
"period": "2025-09",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-10",
"period": "2025-10",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-10",
"period": "2025-10",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-10",
"period": "2025-10",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-11",
"period": "2025-11",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-11",
"period": "2025-11",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-11",
"period": "2025-11",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2025-12",
"period": "2025-12",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2025-12",
"period": "2025-12",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2025-12",
"period": "2025-12",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2026-01",
"period": "2026-01",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2026-01",
"period": "2026-01",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2026-01",
"period": "2026-01",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2026-02",
"period": "2026-02",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2026-02",
"period": "2026-02",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2026-02",
"period": "2026-02",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2026-03",
"period": "2026-03",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2026-03",
"period": "2026-03",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2026-03",
"period": "2026-03",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-railiance01-2026-03",
"period": "2026-03",
"vendor": "hosting.railiance01",
"description": "railiance01 virtual server hosting",
"cost_class": "infrastructure",
"amount": "8.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2026-04",
"period": "2026-04",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2026-04",
"period": "2026-04",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2026-04",
"period": "2026-04",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-railiance01-2026-04",
"period": "2026-04",
"vendor": "hosting.railiance01",
"description": "railiance01 virtual server hosting",
"cost_class": "infrastructure",
"amount": "8.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2026-05",
"period": "2026-05",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2026-05",
"period": "2026-05",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2026-05",
"period": "2026-05",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-railiance01-2026-05",
"period": "2026-05",
"vendor": "hosting.railiance01",
"description": "railiance01 virtual server hosting",
"cost_class": "infrastructure",
"amount": "8.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-social-2026-06",
"period": "2026-06",
"vendor": "domain.coulomb.social",
"description": "coulomb.social (.social) 3.75 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.75",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-domain-pro-2026-06",
"period": "2026-06",
"vendor": "domain.coulomb.pro",
"description": "coulomb.pro (.pro) 3.00 EUR/mo incl. 0.14 EUR/yr ICANN",
"cost_class": "infrastructure",
"amount": "3.00",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-coulombcore-2026-06",
"period": "2026-06",
"vendor": "hosting.coulombcore",
"description": "coulombcore virtual server hosting",
"cost_class": "infrastructure",
"amount": "13.99",
"currency": "EUR",
"source": "invoice"
},
{
"id": "exp-hosting-railiance01-2026-06",
"period": "2026-06",
"vendor": "hosting.railiance01",
"description": "railiance01 virtual server hosting",
"cost_class": "infrastructure",
"amount": "8.99",
"currency": "EUR",
"source": "invoice"
}
],
"note": "Infrastructure expense ledger: domains and virtual server hosting. Totals computed by observatory/ledger.py."
}

View File

@@ -0,0 +1,24 @@
{
"policy_id": "coulomb-governance-v1",
"max_self_serve_discount_pct": "10",
"max_customer_visible_price_increase_pct": "15",
"max_active_experiments": 2,
"max_concurrent_candidate_rollouts": 1,
"require_approval_for_candidate_rollout": true,
"require_approval_for_approximate_provider_mapping": true,
"block_unsupported_provider_artifacts": true,
"drift_blocks_execution": true,
"require_approval_for_price_change": true,
"require_customer_notice_for_price_increase": true,
"customer_notice_days": 30,
"grandfather_existing_customers": true,
"customer_visible_tuning_enabled": false,
"customer_visible_tuning_requires_active_model": true,
"communication_owner_role": "operator",
"default_approver_role": "operator",
"metadata": {
"active_experiment_count": 0,
"candidate_rollout_count": 0,
"policy_scope": "coulomb-social-mvp"
}
}

View File

@@ -0,0 +1,12 @@
{
"exported_at": "2026-06-22",
"users": [
{
"bubble_id": "bubble-tegwick-001",
"username": "tegwick",
"status": "Active",
"created": "2025-11-03",
"plan": "flat-899-eur-monthly"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"usage": [
{
"period": "2026-06",
"user_id": "member-tegwick",
"model": "anthropic/claude-3-haiku",
"tokens": 48200,
"cost_usd": "0.06"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"charges": [
{
"id": "pay-2026-06",
"period": "2026-06",
"gross": "8.99",
"fee": "0.44",
"net": "8.55",
"currency": "EUR",
"customer": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
}
]
}

View File

@@ -0,0 +1,26 @@
{
"version": 1,
"currency": "EUR",
"domains": [
{
"name": "coulomb.social",
"tld": ".social",
"monthly_eur": "3.75",
"icann_fee_annual_eur": "0.14",
"billing_period": "2026-01-31/2027-01-30",
"annual_total_eur": "45.00",
"vat_rate": "19.0"
},
{
"name": "coulomb.pro",
"tld": ".pro",
"monthly_eur": "3.00",
"icann_fee_annual_eur": "0.14",
"billing_period": "2026-01-31/2027-01-30",
"annual_total_eur": "36.00",
"vat_rate": "19.0"
}
],
"monthly_total_eur": "6.75",
"note": "Reference catalog from registrar invoices. Expense ledger rows live in expense_records.json."
}

View File

@@ -0,0 +1,13 @@
{
"version": 1,
"currency": "EUR",
"payout_account": "binky-hedgehog",
"membership": {
"product": "coulomb.social-membership",
"gross_monthly_eur": "8.99",
"fee_monthly_eur": "0.44",
"net_payout_monthly_eur": "8.55",
"member_username": "tegwick"
},
"note": "Reference from actual Stripe payouts. Payment rows live in payment_records.json."
}

View File

@@ -0,0 +1,19 @@
{
"version": 1,
"currency": "EUR",
"servers": [
{
"name": "coulombcore",
"monthly_eur": "13.99",
"started": "2025-01",
"description": "Virtual server hosting for coulombcore"
},
{
"name": "railiance01",
"monthly_eur": "8.99",
"started": "2026-03",
"description": "Virtual server hosting for railiance01"
}
],
"note": "Reference catalog. Monthly expense rows live in expense_records.json."
}

View File

@@ -0,0 +1,76 @@
{
"version": 1,
"currency": "EUR",
"horizon_months": 24,
"monthly_discount_rate_pct": "1.0",
"required_improvement_factor": "1.05",
"profiles": [
{
"id": "solo-builder",
"name": "Solo builder",
"segment": "coulomb-social-members",
"eligible_model_ids": [
"flat-899-eur-monthly",
"membership-plus-credits",
"membership-plus-overage"
],
"members_per_customer": 1,
"expected_monthly_usage_units": "48200",
"usage_variance_pct": "20",
"monthly_churn_pct": "6.0",
"monthly_default_pct": "1.0",
"monthly_support_cost": "0.25",
"monthly_risk_cost": "0.10",
"acquisition_cost": "2.00",
"upfront_investment_cost": "0.00",
"allocated_fixed_cost": "5.00",
"notes": "Calibrated from the current founding-member usage record and payment ledger."
},
{
"id": "small-team",
"name": "Small product team",
"segment": "coulomb-social-members",
"eligible_model_ids": [
"flat-899-eur-monthly",
"membership-plus-credits",
"membership-plus-overage"
],
"members_per_customer": 3,
"expected_monthly_usage_units": "180000",
"usage_variance_pct": "35",
"monthly_churn_pct": "3.5",
"monthly_default_pct": "1.0",
"monthly_support_cost": "1.50",
"monthly_risk_cost": "0.20",
"acquisition_cost": "8.00",
"upfront_investment_cost": "1.50",
"allocated_fixed_cost": "12.00",
"notes": "Hypothesis scenario for a higher-usage small team considering a multi-seat relationship."
}
],
"sensitivity_cases": [
{
"id": "usage-downside",
"name": "Usage downside",
"usage_multiplier": "0.75",
"monthly_churn_delta_pct": "1.5",
"monthly_risk_cost_delta": "0.05"
},
{
"id": "usage-upside",
"name": "Usage upside",
"usage_multiplier": "1.35",
"usage_variance_delta_pct": "10.0"
},
{
"id": "risk-spike",
"name": "Risk spike",
"usage_multiplier": "1.00",
"monthly_churn_delta_pct": "3.0",
"monthly_default_delta_pct": "1.0",
"monthly_support_cost_delta": "0.25",
"monthly_risk_cost_delta": "0.20"
}
],
"notes": "First-pass comparable-customer LTV assumptions for the Coulomb observatory. These scenarios are meant for simulation and comparison, not billing execution."
}

View File

@@ -0,0 +1,56 @@
{
"version": 1,
"currency": "EUR",
"last_reviewed": "2026-06",
"alternatives": [
{
"id": "github-pro",
"name": "GitHub Pro",
"category": "indirect",
"price_monthly_eur": "4.00",
"billing": "monthly",
"features": ["private repos", "protected branches", "CI minutes"],
"signal_level": "S2",
"observed": "2026-06",
"source": "public pricing page",
"note": "Substitute for repository hosting; lacks community and pricing-intelligence positioning."
},
{
"id": "circle-community",
"name": "Circle (community platform)",
"category": "indirect",
"price_monthly_eur": "49.00",
"billing": "monthly",
"features": ["courses", "events", "member spaces", "payments"],
"signal_level": "S2",
"observed": "2026-06",
"source": "public pricing page",
"note": "Higher floor for hosted community; Coulomb is narrower and operator-led."
},
{
"id": "patreon-creator",
"name": "Patreon (creator membership)",
"category": "substitute",
"price_monthly_eur": "8.00",
"billing": "monthly",
"features": ["member posts", "Discord integration", "paywall"],
"signal_level": "S1",
"observed": "2026-06",
"source": "public pricing page",
"note": "Comparable entry price point; different delivery model (content vs repo access)."
},
{
"id": "build-in-house",
"name": "Build in-house community",
"category": "workaround",
"price_monthly_eur": "0.00",
"billing": "operator time",
"features": ["self-hosted forum", "manual onboarding", "no shared pricing lab"],
"signal_level": "S3",
"observed": "2026-06",
"source": "operator estimate",
"note": "Hidden cost in time; common alternative for technical founders."
}
],
"notes": "Market signals are manually curated until competitive intelligence imports exist."
}

View File

@@ -0,0 +1,17 @@
{
"version": 1,
"snapshot_date": "2026-06-22",
"members": [
{
"id": "member-tegwick",
"username": "tegwick",
"external_id": null,
"status": "active",
"joined_at": "2025-11-03",
"plan_id": "flat-899-eur-monthly",
"source": "stripe",
"note": "Sole paying member; coulomb.social membership 8.99 EUR/mo"
}
],
"note": "Infrastructure costs from January 2025; member payments from November 2025."
}

View File

@@ -0,0 +1,118 @@
{
"version": 2,
"records": [
{
"id": "pay-2025-11",
"period": "2025-11",
"gross_amount": "8.99",
"fees_amount": "0.44",
"refunds_amount": "0.00",
"net_amount": "8.55",
"currency": "EUR",
"source": "stripe",
"member_count": 1,
"member_username": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
},
{
"id": "pay-2025-12",
"period": "2025-12",
"gross_amount": "8.99",
"fees_amount": "0.44",
"refunds_amount": "0.00",
"net_amount": "8.55",
"currency": "EUR",
"source": "stripe",
"member_count": 1,
"member_username": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
},
{
"id": "pay-2026-01",
"period": "2026-01",
"gross_amount": "8.99",
"fees_amount": "0.44",
"refunds_amount": "0.00",
"net_amount": "8.55",
"currency": "EUR",
"source": "stripe",
"member_count": 1,
"member_username": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
},
{
"id": "pay-2026-02",
"period": "2026-02",
"gross_amount": "8.99",
"fees_amount": "0.44",
"refunds_amount": "0.00",
"net_amount": "8.55",
"currency": "EUR",
"source": "stripe",
"member_count": 1,
"member_username": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
},
{
"id": "pay-2026-03",
"period": "2026-03",
"gross_amount": "8.99",
"fees_amount": "0.44",
"refunds_amount": "0.00",
"net_amount": "8.55",
"currency": "EUR",
"source": "stripe",
"member_count": 1,
"member_username": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
},
{
"id": "pay-2026-04",
"period": "2026-04",
"gross_amount": "8.99",
"fees_amount": "0.44",
"refunds_amount": "0.00",
"net_amount": "8.55",
"currency": "EUR",
"source": "stripe",
"member_count": 1,
"member_username": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
},
{
"id": "pay-2026-05",
"period": "2026-05",
"gross_amount": "8.99",
"fees_amount": "0.44",
"refunds_amount": "0.00",
"net_amount": "8.55",
"currency": "EUR",
"source": "stripe",
"member_count": 1,
"member_username": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
},
{
"id": "pay-2026-06",
"period": "2026-06",
"gross_amount": "8.99",
"fees_amount": "0.44",
"refunds_amount": "0.00",
"net_amount": "8.55",
"currency": "EUR",
"source": "stripe",
"member_count": 1,
"member_username": "tegwick",
"product": "coulomb.social-membership",
"payout_account": "binky-hedgehog"
}
],
"note": "Stripe payment ledger for tegwick coulomb.social membership (8.99 EUR gross, 0.44 EUR fees, 8.55 EUR net payout to binky-hedgehog)."
}

View File

@@ -0,0 +1,206 @@
{
"version": 1,
"models": [
{
"id": "flat-899-eur-monthly",
"name": "Standard Membership",
"model_type": "flat_subscription",
"lifecycle_phase": "growth",
"currency": "EUR",
"description": "Current flat membership offer for Coulomb Social.",
"access_fee_amount": "8.99",
"access_fee_cadence": "monthly",
"included_usage": "unlimited_repository_access",
"status": "active",
"charge_components": [
{
"id": "membership-access",
"kind": "access",
"amount": "8.99",
"cadence": "monthly",
"label": "Standard membership access fee",
"billing_treatment": "recurring",
"metadata": {
"included_usage": "unlimited_repository_access"
}
}
],
"commitments": [
{
"id": "baseline-term",
"kind": "contract_duration",
"value": "1",
"unit": "month",
"description": "Baseline self-serve monthly term."
}
],
"tunable_parameters": [],
"eligibility": [
"coulomb-social-members"
],
"provider_hints": {
"stripe": {
"collection_method": "charge_automatically"
}
},
"metadata": {
"catalog_version": "canonical-v1"
}
},
{
"id": "membership-plus-credits",
"name": "Membership + AI Credits",
"model_type": "hybrid_subscription_usage",
"lifecycle_phase": "exploration",
"currency": "EUR",
"description": "Candidate model bundling recurring access with a monthly AI allowance.",
"access_fee_amount": "8.99",
"access_fee_cadence": "monthly",
"included_usage": "monthly_ai_credit_allowance",
"status": "candidate",
"charge_components": [
{
"id": "membership-access",
"kind": "access",
"amount": "8.99",
"cadence": "monthly",
"label": "Membership base fee",
"billing_treatment": "recurring"
},
{
"id": "ai-credit-allowance",
"kind": "usage",
"meter": "openrouter_tokens",
"unit": "tokens",
"included_units": "100000",
"label": "Included monthly AI token allowance",
"billing_treatment": "included",
"metadata": {
"included_usage": "monthly_ai_credit_allowance"
}
}
],
"commitments": [
{
"id": "credit-prepay-window",
"kind": "prepayment",
"value": "1",
"unit": "month",
"description": "Allowance resets monthly in the observatory prototype."
}
],
"tunable_parameters": [
{
"key": "included_tokens",
"parameter_class": "seller_controlled",
"data_type": "integer",
"description": "Included OpenRouter token allowance for the monthly bundle.",
"default_value": "100000",
"min_value": "50000",
"max_value": "500000"
},
{
"key": "monthly_allowance_eur",
"parameter_class": "calculated",
"data_type": "decimal",
"description": "Observatory-only euro allowance derived from provider usage cost.",
"default_value": "2.00"
}
],
"eligibility": [
"coulomb-social-members"
],
"provider_hints": {
"stripe": {
"metered_usage_strategy": "future_adapter"
}
},
"metadata": {
"catalog_version": "canonical-v1"
}
},
{
"id": "membership-plus-overage",
"name": "Membership + Overage",
"model_type": "hybrid_subscription_usage",
"lifecycle_phase": "exploration",
"currency": "EUR",
"description": "Candidate model pairing recurring access with included tokens and metered overage.",
"access_fee_amount": "8.99",
"access_fee_cadence": "monthly",
"included_usage": "monthly_ai_credit_allowance",
"overage_meter": "openrouter_tokens",
"status": "candidate",
"charge_components": [
{
"id": "membership-access",
"kind": "access",
"amount": "8.99",
"cadence": "monthly",
"label": "Membership base fee",
"billing_treatment": "recurring"
},
{
"id": "ai-overage-usage",
"kind": "usage",
"meter": "openrouter_tokens",
"unit": "tokens",
"included_units": "100000",
"unit_price": "0.002",
"label": "OpenRouter token overage",
"billing_treatment": "metered",
"metadata": {
"included_usage": "monthly_ai_credit_allowance"
}
}
],
"commitments": [
{
"id": "baseline-term",
"kind": "contract_duration",
"value": "1",
"unit": "month",
"description": "Baseline monthly term; solver can later trade this against usage economics."
}
],
"tunable_parameters": [
{
"key": "included_tokens",
"parameter_class": "customer_tunable",
"data_type": "integer",
"description": "Customer-selectable included token allowance within seller-approved bounds.",
"default_value": "100000",
"min_value": "50000",
"max_value": "300000"
},
{
"key": "contract_duration_months",
"parameter_class": "customer_tunable",
"data_type": "integer",
"description": "Longer term can support improved usage pricing in later solver milestones.",
"default_value": "1",
"min_value": "1",
"max_value": "12"
},
{
"key": "overage_unit_price",
"parameter_class": "calculated",
"data_type": "decimal",
"description": "Current observatory overage rate derived from scenario assumptions.",
"default_value": "0.002"
}
],
"eligibility": [
"coulomb-social-members"
],
"provider_hints": {
"stripe": {
"meter_name": "openrouter_tokens"
}
},
"metadata": {
"catalog_version": "canonical-v1"
}
}
]
}

View File

@@ -0,0 +1,12 @@
{
"id": "coulomb-social-membership",
"name": "Coulomb Social Membership",
"lifecycle_phase": "growth",
"currency": "EUR",
"description": "Repository and community access via monthly subscription.",
"active_pricing_model_id": "flat-899-eur-monthly",
"channels": {
"membership_platform": "bubble.io",
"payments": "stripe"
}
}

View File

@@ -0,0 +1,38 @@
{
"version": 1,
"requests": [
{
"id": "small-team-lower-usage-price",
"name": "Small team lower usage price",
"profile_id": "small-team",
"model_id": "membership-plus-overage",
"preference": "lower_usage_price",
"approval_mode": "self_serve_only",
"selected_tunables": {
"included_tokens": "50000",
"contract_duration_months": 3
},
"search_policy": {
"min_usage_unit_price": "0.0005",
"usage_unit_price_step": "0.0001"
}
},
{
"id": "small-team-high-included-bundle",
"name": "Small team high included bundle",
"profile_id": "small-team",
"model_id": "membership-plus-overage",
"preference": "lower_usage_price",
"approval_mode": "self_serve_only",
"selected_tunables": {
"included_tokens": "150000",
"contract_duration_months": 3
},
"search_policy": {
"min_usage_unit_price": "0.0005",
"usage_unit_price_step": "0.0001"
}
}
],
"notes": "Customer-tuning pilot requests for the Coulomb hybrid overage prototype."
}

View File

@@ -0,0 +1,16 @@
{
"version": 1,
"fx_usd_eur": "0.92",
"records": [
{
"id": "usage-2026-06-tegwick",
"period": "2026-06",
"member_id": "member-tegwick",
"model": "anthropic/claude-3-haiku",
"tokens": 48200,
"cost_usd": "0.06",
"cost_eur": "0.06",
"source": "openrouter"
}
]
}

View File

@@ -0,0 +1,40 @@
{
"version": 1,
"currency": "EUR",
"product_id": "coulomb-social-membership",
"segments": [
{
"id": "solo-builder",
"name": "Solo builder",
"low_eur": "8.99",
"high_eur": "19.00",
"confidence": "hypothesis",
"drivers": ["private repository access", "async community", "low switching cost"],
"evidence": "Founding member joined at list price without negotiation."
},
{
"id": "small-team",
"name": "Small product team",
"low_eur": "12.00",
"high_eur": "29.00",
"confidence": "hypothesis",
"drivers": ["shared norms", "pricing experiment sandbox", "operator proximity"],
"evidence": "No team-tier offer yet; inferred from comparable community memberships."
}
],
"value_drivers": [
{
"id": "repo-access",
"label": "Repository & community access",
"strength": "S2",
"note": "Core deliverable today; differentiated by operator involvement, not feature breadth."
},
{
"id": "pricing-lab",
"label": "Live pricing laboratory",
"strength": "S1",
"note": "Members observe real operator economics; value increases as observatory matures."
}
],
"notes": "Value range is observatory-only until willingness-to-pay interviews and usage signals exist."
}

View File

@@ -0,0 +1,58 @@
# Economic Observatory UI — whynot-design workflow
Build UI from what exists in **whynot-design** (`~/whynot-design` or
`gitea:whynot/whynot-design`). Do not invent parallel components or tokens.
## Before you build
1. **Sync the vendor tree**`make design` (optional `REF=<tag-or-sha>`).
2. **Browse upstream** — inspect `src/elements/` and `src/styles/` in
whynot-design for an existing `<wn-*>` element or `.wn-*` class that fits.
3. **Check the atelier mock** — layout reference only:
https://claude.ai/design/p/fb2eef8c-c1fc-4c75-bff4-3782552e5511
## Layer model
| Layer | Source | Observatory usage |
|-------|--------|-------------------|
| **Layer 1** | `colors_and_type.css`, `components.css`, `tokens/*.json` | Global look; CSS variables (`--paper`, `--sp-*`, `--fg-*`) |
| **Layer 2** | `index.js` + `elements/*.js` (Lit) | App chrome: `wn-top-nav`, `wn-sidebar`, `wn-card`, `wn-field-row`, … |
| **Layer 3** | `ui/styles.css`, `ui/app.js` | Observatory-only layout (`.obs-*`) and data binding |
Layer 3 must consume Layer 1 tokens and Layer 2 components. Avoid gradients,
card shadows, and colours outside the token set.
## Implementation rules
1. **Prefer `<wn-*>` over custom markup** — use `wn-card`, `wn-eyebrow`,
`wn-banner`, `wn-tag`, `wn-field-row`, `wn-table--native` before adding new
patterns.
2. **Extend with `.obs-*`, not `.wn-*`** — never edit vendored CSS/JS; add
layout in `ui/styles.css` only.
3. **Tables** — use `<table class="wn-table--native">` for ledger data; the
shadow-DOM `wn-table` is for Lit-only grids.
4. **Typography** — use `.lead`, `.small`, `.mono` from Layer 1; metric values
use `.obs-metric__value` with `font-variant-numeric: tabular-nums`.
5. **New upstream needs** — add the component or token in whynot-design first,
then `make design` to vendor it here. Do not fork one-off elements into
`ui/`.
## File touch map
| Change | Files |
|--------|-------|
| New section / panel | `ui/index.html`, `ui/app.js`, maybe `ui/styles.css` |
| Chrome or shared widget | whynot-design repo → `make design` |
| Observatory layout only | `ui/styles.css` |
| Data for a panel | `observatory/api.py` + tests |
## Verify
```bash
cd projects/coulomb-pricing
make design # if vendor changed
make test
make serve # http://127.0.0.1:8765/
```
Footer shows pinned ref from `ui/vendor/whynot-design/.whynot-design-ref`.

View File

@@ -0,0 +1,3 @@
"""Coulomb Social Economic Observatory — MVP (ledger, API, importers, simulator)."""
__version__ = "0.1.0"

View File

@@ -0,0 +1,3 @@
from .dashboard import main
raise SystemExit(main())

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
def ensure_repo_root_on_syspath() -> None:
root = str(REPO_ROOT)
if root not in sys.path:
sys.path.insert(0, root)

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from decimal import Decimal
from .models import EconomicsSnapshot
from .usage import build_usage_summary
def build_cost_allocation(
snapshot: EconomicsSnapshot,
usage_records: list[dict],
) -> dict:
usage = build_usage_summary(usage_records, snapshot.period)
variable_ai = usage["total_ai_spend_eur"]
fixed = snapshot.monthly_infrastructure_cost
variable_processing = snapshot.monthly_payment_processing_cost
total = fixed + variable_processing + variable_ai
contribution = snapshot.monthly_revenue - total
return {
"period": snapshot.period,
"currency": snapshot.currency,
"fixed_costs_eur": fixed,
"variable_processing_eur": variable_processing,
"variable_ai_eur": variable_ai,
"total_platform_cost_eur": total,
"cost_floor_eur": snapshot.cost_per_member,
"contribution_margin_eur": contribution,
"contribution_margin_pct": snapshot.gross_margin_pct,
"cost_per_member_eur": snapshot.cost_per_member,
"active_members": snapshot.active_members,
}

View File

@@ -0,0 +1,193 @@
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
from typing import Any
from .economics import build_liquidity_summary, build_snapshot
from .load import (
default_data_dir,
latest_period,
load_governance_policy,
load_ltv_scenarios,
load_budget,
load_expense_records,
load_market_signals,
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
load_tuning_requests,
load_value_range,
)
from .allocation import build_cost_allocation
from .boundary import build_boundary_validation
from .credits import build_credit_summary, load_credit_wallets
from .governance import build_governance_policy, build_governance_surfaces
from .membership_analytics import build_membership_analytics
from .pricing_context import build_cost_floor, build_market_price_view, build_value_range_view
from .publication import build_stripe_publication_preview
from .recommendations import build_pricing_recommendations
from .simulator import build_pricing_simulations
from .tuning import build_customer_tuning_pilot
from .usage import build_usage_summary, load_usage_records
def _serialize(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {key: _serialize(getattr(value, key)) for key in value.__dataclass_fields__}
if isinstance(value, tuple):
return [_serialize(item) for item in value]
if isinstance(value, list):
return [_serialize(item) for item in value]
if isinstance(value, dict):
return {key: _serialize(item) for key, item in value.items()}
return value
def _load_json_catalog(data_dir: Path, name: str) -> dict:
path = data_dir / "infrastructure" / name
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def build_dashboard_payload(data_dir: Path | None = None, period: str | None = None) -> dict:
root = data_dir or default_data_dir()
product = load_product(root)
budget = load_budget(root)
models = load_pricing_models(root)
members = load_membership(root)
payments = load_payment_records(root)
expenses = load_expense_records(root)
ledger = load_monthly_ledger(root)
target_period = period or latest_period(ledger)
snapshot = build_snapshot(target_period, product, models, members, payments, ledger)
liquidity = build_liquidity_summary(budget, payments, ledger, target_period)
payment_by_period = {record.period: record for record in payments}
history = []
for month in sorted(ledger, key=lambda row: row.period):
if month.period > target_period:
continue
payment = payment_by_period.get(month.period)
net_payment = payment.net_amount if payment else Decimal("0")
history.append(
{
"period": month.period,
"active_members": month.active_members,
"gross_revenue": month.gross_revenue,
"infrastructure_cost": month.infrastructure_cost,
"payment_processing_cost": month.payment_processing_cost,
"total_platform_cost": month.total_platform_cost,
"net_payment": net_payment,
"net_liquidity": net_payment - month.infrastructure_cost,
}
)
value_range_raw = load_value_range(root)
market_raw = load_market_signals(root)
usage_records = load_usage_records(root)
usage_summary = build_usage_summary(usage_records, target_period)
governance_policy_raw = load_governance_policy(root)
ltv_scenarios = load_ltv_scenarios(root)
tuning_requests = load_tuning_requests(root)
governance_policy = build_governance_policy(governance_policy_raw)
cost_floor = build_cost_floor(snapshot, models)
value_range = build_value_range_view(value_range_raw, snapshot, product, models)
market_price = build_market_price_view(market_raw)
cost_allocation = build_cost_allocation(snapshot, usage_records)
ai_cost_per_member = usage_summary["cost_per_active_user_eur"]
simulations = build_pricing_simulations(
snapshot,
models,
ai_cost_per_member,
usage_records=usage_records,
scenario_catalog=ltv_scenarios,
)
customer_tuning = build_customer_tuning_pilot(
snapshot,
models,
usage_records,
ltv_scenarios,
tuning_requests,
)
boundary_validation = build_boundary_validation(snapshot, models, usage_records)
provider_publication = build_stripe_publication_preview(
product,
models,
root,
model_id=product.active_pricing_model_id,
)
credit_wallets = load_credit_wallets(root)
credit_summary = build_credit_summary(
credit_wallets,
{key: value for key, value in usage_summary["by_member"].items()},
target_period,
)
recommendations = build_pricing_recommendations(
cost_floor,
value_range,
market_price,
simulations,
usage_summary,
boundary_validation=boundary_validation,
customer_tuning=customer_tuning,
provider_publication=provider_publication,
governance_policy=governance_policy_raw,
product=product,
)
governance = build_governance_surfaces(
root,
product,
models,
cost_floor,
customer_tuning,
provider_publication,
governance_policy,
)
return _serialize(
{
"design_reference": "https://claude.ai/design/p/fb2eef8c-c1fc-4c75-bff4-3782552e5511",
"period": target_period,
"product": product,
"budget": budget,
"snapshot": snapshot,
"liquidity": liquidity,
"history": history,
"pricing_models": models,
"members": members,
"payments": payments,
"expense_record_count": len(expenses),
"membership_analytics": build_membership_analytics(
members, target_period, [row["period"] for row in history]
),
"cost_floor": cost_floor,
"value_range": value_range,
"market_price": market_price,
"usage": usage_summary,
"cost_allocation": cost_allocation,
"pricing_simulations": simulations,
"customer_tuning": customer_tuning,
"boundary_validation": boundary_validation,
"credit_wallets": credit_summary,
"recommendations": recommendations,
"governance": governance,
"provider_publication": provider_publication,
"infrastructure": {
"domains": _load_json_catalog(root, "domains.json"),
"virtual_servers": _load_json_catalog(root, "virtual_servers.json"),
"stripe": _load_json_catalog(root, "stripe.json"),
},
}
)
def payload_json(data_dir: Path | None = None, period: str | None = None) -> str:
return json.dumps(build_dashboard_payload(data_dir, period), indent=2)

View File

@@ -0,0 +1,105 @@
from __future__ import annotations
from decimal import Decimal
from typing import Any
from .models import EconomicsSnapshot, PricingModel
from ._repo_root import ensure_repo_root_on_syspath
ensure_repo_root_on_syspath()
from adaptive_pricing_core.boundary_engine import ( # noqa: E402
BoundaryPolicy,
PricingConfiguration,
validate_pricing_configuration,
)
def _usage_unit_cost(records: list[dict[str, Any]], period: str) -> Decimal:
period_rows = [row for row in records if row.get("period") == period]
total_tokens = sum(Decimal(str(row.get("tokens", "0"))) for row in period_rows)
total_cost = sum(Decimal(str(row.get("cost_eur", "0"))) for row in period_rows)
if total_tokens <= Decimal("0"):
return Decimal("0")
return total_cost / total_tokens
def _total_usage_units(records: list[dict[str, Any]], period: str) -> Decimal:
return sum(Decimal(str(row.get("tokens", "0"))) for row in records if row.get("period") == period)
def build_boundary_policy(snapshot: EconomicsSnapshot) -> BoundaryPolicy:
return BoundaryPolicy(
minimum_margin_pct=Decimal("0"),
target_margin_pct=Decimal("15"),
max_payment_fee_pct=Decimal("10"),
max_expected_usage_variance_pct=Decimal("50"),
approval_discount_pct=Decimal("10"),
max_discount_pct=Decimal("25"),
minimum_contract_duration_for_discount_months=3,
minimum_turnover_multiple_for_discount=Decimal("1"),
minimum_prepayment_months_for_discount=Decimal("1"),
)
def build_boundary_validation(
snapshot: EconomicsSnapshot,
models: list[PricingModel],
usage_records: list[dict[str, Any]],
segment: str | None = None,
) -> dict[str, Any]:
fee_rate_pct = Decimal("0")
if snapshot.monthly_revenue > Decimal("0"):
fee_rate_pct = (
snapshot.monthly_payment_processing_cost / snapshot.monthly_revenue
) * Decimal("100")
unit_cost = _usage_unit_cost(usage_records, snapshot.period)
usage_units = _total_usage_units(usage_records, snapshot.period)
direct_usage_cost = sum(
Decimal(str(row.get("cost_eur", "0")))
for row in usage_records
if row.get("period") == snapshot.period
)
allocated_fixed_cost = (
snapshot.monthly_infrastructure_cost / snapshot.active_members
if snapshot.active_members
else snapshot.monthly_infrastructure_cost
)
policy = build_boundary_policy(snapshot)
model_results = []
for model in models:
has_usage_component = any(component.kind == "usage" for component in model.charge_components)
configuration = PricingConfiguration(
model=model,
segment=segment or (model.eligibility[0] if model.eligibility else None),
expected_usage_units=usage_units if has_usage_component else Decimal("0"),
expected_usage_variance_pct=Decimal("25"),
allocated_fixed_cost=allocated_fixed_cost,
direct_cost_amount=Decimal("0") if has_usage_component else direct_usage_cost,
unit_cost=unit_cost if has_usage_component else Decimal("0"),
payment_fee_rate_pct=fee_rate_pct,
)
model_results.append(validate_pricing_configuration(configuration, policy))
return {
"period": snapshot.period,
"policy": policy,
"assumptions": {
"segment": segment or "model-default-eligibility",
"observed_usage_units": usage_units,
"observed_usage_unit_cost": unit_cost,
"direct_usage_cost_for_flat_models": direct_usage_cost,
"allocated_fixed_cost_per_member": allocated_fixed_cost,
"payment_fee_rate_pct": fee_rate_pct,
"expected_usage_variance_pct": Decimal("25"),
},
"model_results": model_results,
"notes": [
"This MVP policy uses current observatory economics and conservative defaults rather than a seller-specific governance file.",
"Self-serve discounts above 10% require approval; discounts above 25% are rejected.",
"Term-only concessions require at least a 3-month contract to count as meaningful commitment support.",
],
}

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
from typing import Any
from .load import _read_json, default_data_dir
TWOPLACES = Decimal("0.01")
def _money(value: Decimal) -> Decimal:
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
def load_credit_wallets(data_dir=None) -> dict[str, Any]:
root = data_dir or default_data_dir()
path = Path(root) / "credit_wallets.json"
if not path.exists():
return {"version": 1, "currency": "EUR", "wallets": []}
return _read_json(path)
def build_credit_summary(raw: dict, usage_by_member: dict[str, Decimal], period: str) -> dict:
wallets = []
for item in raw.get("wallets", []):
member_id = item["member_id"]
allowance = Decimal(str(item.get("monthly_allowance_eur", "0")))
used = usage_by_member.get(member_id, Decimal(str(item.get("used_eur", "0"))))
remaining = max(Decimal("0"), allowance - used)
wallets.append(
{
"member_id": member_id,
"period": period,
"monthly_allowance_eur": _money(allowance),
"used_eur": _money(used),
"remaining_eur": _money(remaining),
"overage_eur": _money(max(Decimal("0"), used - allowance)),
}
)
return {
"period": period,
"currency": raw.get("currency", "EUR"),
"wallet_count": len(wallets),
"wallets": wallets,
"notes": "Observatory-only credit accounting; no customer billing in MVP.",
}

View File

@@ -0,0 +1,170 @@
from __future__ import annotations
import argparse
from decimal import Decimal
from pathlib import Path
from .economics import build_liquidity_summary, build_snapshot
from .load import (
default_data_dir,
latest_period,
load_budget,
load_expense_records,
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
)
from .models import EconomicsSnapshot, LiquiditySummary, MonthlyPlatformCost, PaymentRecord, PricingModel, Product
def _history_rows(
monthly_costs: list[MonthlyPlatformCost],
payments: list[PaymentRecord],
through_period: str,
) -> str:
payment_by_period = {record.period: record for record in payments}
lines: list[str] = []
for month in sorted(monthly_costs, key=lambda item: item.period):
if month.period > through_period:
continue
payment = payment_by_period.get(month.period)
net_payment = payment.net_amount if payment else Decimal("0")
period_net = net_payment - month.infrastructure_cost
lines.append(
f"| {month.period} | {month.active_members} | {month.gross_revenue} | "
f"{month.infrastructure_cost} | {month.payment_processing_cost} | "
f"{month.total_platform_cost} | {period_net:.2f} |"
)
return "\n".join(lines)
def render_dashboard(
product: Product,
models: list[PricingModel],
snapshot: EconomicsSnapshot,
liquidity: LiquiditySummary,
monthly_costs: list[MonthlyPlatformCost],
payments: list[PaymentRecord],
expense_count: int,
) -> str:
active = next(m for m in models if m.id == product.active_pricing_model_id)
registry_lines = "\n".join(
f"| {model.id} | {model.name} | {model.model_type} | {model.status} |"
for model in models
)
history_lines = _history_rows(monthly_costs, payments, snapshot.period)
budget_state = (
"over budget"
if liquidity.remaining_budget < Decimal("0")
else "within budget"
)
return f"""# Economics Dashboard v1 — {product.name}
**Period:** {snapshot.period}
**Lifecycle phase:** {product.lifecycle_phase}
**Active pricing model:** {active.name} ({active.access_fee_amount} {active.currency}/{active.access_fee_cadence})
> Platform costs accrue to the operator. Customer cost-pass-through billing is
> **not active** in MVP — members pay subscription only. All totals are computed
> programmatically from expense and payment record ledgers ({expense_count} expense
> records).
## Key Metrics (current period)
| Metric | Value |
|--------|------:|
| Active members | {snapshot.active_members} |
| Member payments (gross) | {snapshot.monthly_revenue} {snapshot.currency} |
| Infrastructure cost | {snapshot.monthly_infrastructure_cost} {snapshot.currency} |
| Payment processing cost | {snapshot.monthly_payment_processing_cost} {snapshot.currency} |
| Total platform cost | {snapshot.monthly_total_platform_cost} {snapshot.currency} |
| Platform cost per member | {snapshot.cost_per_member} {snapshot.currency} |
| Period gross margin | {snapshot.gross_margin} {snapshot.currency} |
| Period gross margin % | {snapshot.gross_margin_pct}% |
| Period net liquidity | {snapshot.period_net_liquidity} {snapshot.currency} ({snapshot.liquidity_status}) |
_Period net liquidity = net member payments infrastructure cost (processing fees already netted from payments)._
_Revenue source: {snapshot.revenue_source}_
## Liquidity & Budget (through {liquidity.through_period})
| Metric | Value |
|--------|------:|
| Initial budget | {liquidity.initial_budget} {liquidity.currency} |
| Cumulative member payments (net) | {liquidity.cumulative_member_payments} {liquidity.currency} |
| Cumulative infrastructure cost | {liquidity.cumulative_infrastructure_cost} {liquidity.currency} |
| Cumulative payment processing | {liquidity.cumulative_payment_processing_cost} {liquidity.currency} |
| Cumulative total platform cost | {liquidity.cumulative_total_platform_cost} {liquidity.currency} |
| Cumulative net liquidity | {liquidity.cumulative_net_liquidity} {liquidity.currency} ({liquidity.liquidity_status}) |
| Remaining budget | {liquidity.remaining_budget} {liquidity.currency} ({budget_state}) |
| Months tracked | {liquidity.months_tracked} |
## Monthly History
| Period | Members | Gross revenue | Infrastructure | Processing | Total platform | Net liquidity |
|--------|--------:|--------------:|---------------:|-----------:|---------------:|--------------:|
{history_lines}
## Pricing Model Registry
| ID | Name | Type | Status |
|----|------|------|--------|
{registry_lines}
## Registries Loaded
- Product model (`data/product.json`)
- Budget (`data/budget.json`)
- Expense records (`data/expense_records.json`) — source of truth for costs
- Infrastructure catalog (`data/infrastructure/`) — domain, VPS, and Stripe reference data
- Payment records (`data/payment_records.json`)
- Membership (`data/membership.json`)
- Requirements (`REQUIREMENTS.md`)
"""
def generate_dashboard(data_dir: Path | None = None, period: str | None = None) -> str:
root = data_dir or default_data_dir()
product = load_product(root)
budget = load_budget(root)
models = load_pricing_models(root)
members = load_membership(root)
payments = load_payment_records(root)
expenses = load_expense_records(root)
monthly_costs = load_monthly_ledger(root)
target_period = period or latest_period(monthly_costs)
snapshot = build_snapshot(target_period, product, models, members, payments, monthly_costs)
liquidity = build_liquidity_summary(budget, payments, monthly_costs, target_period)
return render_dashboard(
product, models, snapshot, liquidity, monthly_costs, payments, len(expenses)
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Coulomb Social Economics Dashboard v1")
parser.add_argument("--data-dir", type=Path, default=None, help="Registry data directory")
parser.add_argument("--period", default=None, help="Reporting period (YYYY-MM)")
parser.add_argument(
"--output",
type=Path,
default=None,
help="Write Markdown report to this path (default: stdout only)",
)
args = parser.parse_args(argv)
report = generate_dashboard(args.data_dir, args.period)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(report, encoding="utf-8")
print(f"Wrote {args.output}")
else:
print(report)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,155 @@
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from .models import (
Budget,
EconomicsSnapshot,
LiquidityStatus,
LiquiditySummary,
MembershipRecord,
MonthlyPlatformCost,
PaymentRecord,
PricingModel,
Product,
)
TWOPLACES = Decimal("0.01")
PCTPLACES = Decimal("0.1")
def _quantize(value: Decimal, exp: Decimal = TWOPLACES) -> Decimal:
return value.quantize(exp, rounding=ROUND_HALF_UP)
def liquidity_status_for(net: Decimal) -> LiquidityStatus:
if net < Decimal("0"):
return "burning"
if net > Decimal("0"):
return "generating"
return "neutral"
def active_members(members: list[MembershipRecord]) -> int:
return sum(1 for member in members if member.status == "active")
def active_pricing_model(models: list[PricingModel], product: Product) -> PricingModel:
for model in models:
if model.id == product.active_pricing_model_id:
return model
raise ValueError(f"active pricing model not found: {product.active_pricing_model_id}")
def payment_for_period(period: str, payments: list[PaymentRecord]) -> PaymentRecord | None:
for record in payments:
if record.period == period:
return record
return None
def estimate_monthly_revenue(
period: str,
product: Product,
models: list[PricingModel],
members: list[MembershipRecord],
payments: list[PaymentRecord],
monthly_costs: list[MonthlyPlatformCost],
) -> tuple[Decimal, Decimal, str]:
recorded = payment_for_period(period, payments)
if recorded:
return recorded.gross_amount, recorded.net_amount, recorded.source
month = next((item for item in monthly_costs if item.period == period), None)
if month and month.gross_revenue > 0:
return month.gross_revenue, month.gross_revenue, "derived_from_ledger"
model = active_pricing_model(models, product)
count = active_members(members)
gross = model.access_fee_amount * count
return gross, gross, "derived_from_membership"
def periods_through(target: str, monthly_costs: list[MonthlyPlatformCost]) -> list[str]:
return sorted(item.period for item in monthly_costs if item.period <= target)
def build_liquidity_summary(
budget: Budget,
payments: list[PaymentRecord],
monthly_costs: list[MonthlyPlatformCost],
through_period: str,
) -> LiquiditySummary:
tracked = periods_through(through_period, monthly_costs)
cost_by_period = {item.period: item for item in monthly_costs}
cumulative_payments = Decimal("0")
cumulative_infrastructure = Decimal("0")
cumulative_processing = Decimal("0")
for period in tracked:
month = cost_by_period[period]
cumulative_infrastructure += month.infrastructure_cost
cumulative_processing += month.payment_processing_cost
payment = payment_for_period(period, payments)
cumulative_payments += payment.net_amount if payment else Decimal("0")
cumulative_net = _quantize(cumulative_payments - cumulative_infrastructure)
remaining = _quantize(budget.initial_budget + cumulative_net)
cumulative_total = _quantize(cumulative_infrastructure + cumulative_processing)
return LiquiditySummary(
currency=budget.currency,
through_period=through_period,
initial_budget=budget.initial_budget,
cumulative_member_payments=_quantize(cumulative_payments),
cumulative_infrastructure_cost=_quantize(cumulative_infrastructure),
cumulative_payment_processing_cost=_quantize(cumulative_processing),
cumulative_total_platform_cost=cumulative_total,
cumulative_net_liquidity=cumulative_net,
remaining_budget=remaining,
liquidity_status=liquidity_status_for(cumulative_net),
months_tracked=len(tracked),
)
def build_snapshot(
period: str,
product: Product,
models: list[PricingModel],
members: list[MembershipRecord],
payments: list[PaymentRecord],
monthly_costs: list[MonthlyPlatformCost],
) -> EconomicsSnapshot:
month = next(item for item in monthly_costs if item.period == period)
count = month.active_members if month.active_members else active_members(members)
gross_revenue, net_revenue, revenue_source = estimate_monthly_revenue(
period, product, models, members, payments, monthly_costs
)
infrastructure = month.infrastructure_cost
processing = month.payment_processing_cost
total_platform = month.total_platform_cost
cost_per_member = _quantize(total_platform / count) if count else Decimal("0.00")
gross_margin = _quantize(gross_revenue - total_platform)
margin_pct = (
_quantize((gross_margin / gross_revenue) * Decimal("100"), PCTPLACES)
if gross_revenue
else Decimal("-100.0") if total_platform else Decimal("0.0")
)
period_net = _quantize(net_revenue - infrastructure)
return EconomicsSnapshot(
period=period,
currency=product.currency,
active_members=count,
monthly_revenue=_quantize(gross_revenue),
monthly_infrastructure_cost=infrastructure,
monthly_payment_processing_cost=processing,
monthly_total_platform_cost=total_platform,
cost_per_member=cost_per_member,
gross_margin=gross_margin,
gross_margin_pct=margin_pct,
pricing_model_count=len(models),
revenue_source=revenue_source,
period_net_liquidity=period_net,
liquidity_status=liquidity_status_for(period_net),
)

View File

@@ -0,0 +1,772 @@
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
from typing import Any
from ._repo_root import ensure_repo_root_on_syspath
from .publication import default_stripe_state_path, load_stripe_publication_state
ensure_repo_root_on_syspath()
from adaptive_pricing_core.governance import ( # noqa: E402
ApprovalRequirement,
GovernanceAssessment,
GovernancePolicy,
GovernanceRisk,
HealthCheck,
SafeTuningContract,
SafeTuningExample,
SafeTuningParameter,
SellerRecommendation,
SupportingObservation,
governance_policy_from_dict,
)
TWOPLACES = Decimal("0.01")
def _serialize(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {key: _serialize(getattr(value, key)) for key in value.__dataclass_fields__}
if isinstance(value, tuple):
return [_serialize(item) for item in value]
if isinstance(value, list):
return [_serialize(item) for item in value]
if isinstance(value, dict):
return {key: _serialize(item) for key, item in value.items()}
return value
def _money(value: Decimal) -> Decimal:
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
def _decimal(value: Decimal | str | int | float | None) -> Decimal:
if value in (None, ""):
return Decimal("0")
return Decimal(str(value))
def build_governance_policy(raw: dict[str, Any]) -> GovernancePolicy:
return governance_policy_from_dict(raw)
def _publication_assessment(
product: Any,
provider_publication: dict[str, Any],
policy: GovernancePolicy,
) -> GovernanceAssessment:
artifact_counts = provider_publication.get("artifact_counts", {})
approximate = int(artifact_counts.get("approximate", 0))
unsupported = int(artifact_counts.get("unsupported", 0))
drift_count = len(provider_publication.get("plan", {}).get("drift", []))
model_id = provider_publication.get("model_id")
approvals: list[ApprovalRequirement] = []
risks: list[GovernanceRisk] = []
observations = [
SupportingObservation(
id="provider-artifact-counts",
title="Provider mapping counts",
summary=(
f"{artifact_counts.get('exact', 0)} exact, "
f"{approximate} approximate, {unsupported} unsupported artifacts."
),
source_ref="provider_publication.artifact_counts",
),
SupportingObservation(
id="provider-drift-count",
title="Provider drift findings",
summary=f"{drift_count} provider drift findings in the current publication preview.",
source_ref="provider_publication.plan.drift",
),
]
if model_id != getattr(product, "active_pricing_model_id", None):
approvals.append(
ApprovalRequirement(
id="candidate-rollout-approval",
title="Candidate model rollout approval",
approver_role=policy.default_approver_role,
reason="The provider publication target is not the currently active product pricing model.",
)
)
risks.append(
GovernanceRisk(
id="candidate-rollout",
severity="medium",
summary="The publication target is a candidate model rather than the active production model.",
mitigation="Keep the change in shadow state or route through explicit rollout approval.",
)
)
if approximate > 0:
risks.append(
GovernanceRisk(
id="approximate-provider-mapping",
severity="medium",
summary="Some provider mappings are approximate rather than fully executable in Stripe.",
mitigation="Supplement Stripe publication with operational contract logic or human review.",
)
)
if policy.require_approval_for_approximate_provider_mapping:
approvals.append(
ApprovalRequirement(
id="approximate-provider-approval",
title="Approximate provider mapping approval",
approver_role=policy.default_approver_role,
reason="Customer-visible rollout would rely on approximate Stripe mappings.",
)
)
if unsupported > 0:
risks.append(
GovernanceRisk(
id="unsupported-provider-artifacts",
severity="high",
summary="Some pricing artifacts cannot be executed in Stripe by the current publisher.",
mitigation="Block rollout until the unsupported artifacts are removed or implemented.",
)
)
if drift_count > 0:
risks.append(
GovernanceRisk(
id="provider-drift",
severity="high",
summary="The provider shadow state differs from the desired pricing definition.",
mitigation="Reconcile drift before rollout or retire the unmanaged artifact state.",
)
)
if unsupported > 0 and policy.block_unsupported_provider_artifacts:
return GovernanceAssessment(
decision="blocked",
summary="Blocked: unsupported Stripe mappings remain in the provider publication plan.",
approvals=tuple(approvals),
risks=tuple(risks),
supporting_observations=tuple(observations),
notes=(
"Shadow-state publication may still proceed, but customer-visible execution should remain blocked.",
),
)
if drift_count > 0 and policy.drift_blocks_execution:
return GovernanceAssessment(
decision="blocked",
summary="Blocked: provider drift must be reconciled before execution.",
approvals=tuple(approvals),
risks=tuple(risks),
supporting_observations=tuple(observations),
)
if approvals:
return GovernanceAssessment(
decision="approval_required",
summary="Approval required before customer-visible execution.",
approvals=tuple(approvals),
risks=tuple(risks),
supporting_observations=tuple(observations),
)
return GovernanceAssessment(
decision="proceed",
summary="Provider publication is execution-ready under the current governance policy.",
approvals=(),
risks=tuple(risks),
supporting_observations=tuple(observations),
)
def _risk_from_boundary_result(result: dict[str, Any]) -> GovernanceRisk:
severity = "high" if result["status"] == "fail" else "medium"
return GovernanceRisk(
id=result["id"],
severity=severity,
summary=result["summary"],
mitigation=result.get("suggested_action") or result["reason"],
)
def build_pricing_recommendation_workflow(
cost_floor: dict[str, Any],
value_range: dict[str, Any],
market_price: dict[str, Any],
simulations: dict[str, Any],
usage_summary: dict[str, Any],
*,
boundary_validation: dict[str, Any] | None = None,
customer_tuning: dict[str, Any] | None = None,
provider_publication: dict[str, Any] | None = None,
governance_policy: GovernancePolicy | None = None,
product: Any | None = None,
) -> list[dict[str, Any]]:
policy = governance_policy or GovernancePolicy(policy_id="default-governance-policy")
publication_assessment = _publication_assessment(
product,
provider_publication or {},
policy,
) if provider_publication and product is not None else GovernanceAssessment(
decision="proceed",
summary="No provider publication assessment was supplied.",
approvals=(),
risks=(),
supporting_observations=(),
)
recommendations: list[SellerRecommendation] = []
margin_pct = _decimal(cost_floor.get("gross_margin_pct"))
active_price = _decimal(value_range.get("current_price_eur"))
cost_per_member = _decimal(cost_floor.get("cost_per_member"))
ai_spend = _decimal(usage_summary.get("total_ai_spend_eur"))
if margin_pct < Decimal("10"):
approvals = []
risks = [
GovernanceRisk(
id="customer-communication",
severity="medium",
summary="Changing the membership price affects existing customer expectations.",
mitigation=(
f"Route communication through {policy.communication_owner_role} and honor "
f"{policy.customer_notice_days}-day notice if price increases are tested."
),
)
]
if policy.require_approval_for_price_change:
approvals.append(
ApprovalRequirement(
id="price-change-approval",
title="Price change approval",
approver_role=policy.default_approver_role,
reason="The recommendation would change customer-visible pricing.",
)
)
recommendations.append(
SellerRecommendation(
id="margin-pressure",
recommendation_type="model_change",
priority="high",
title="Margin below 10%",
rationale=f"Gross margin is {margin_pct}% at the current price.",
suggested_action="Review infrastructure cost or test a higher access fee within value-range bands.",
confidence=Decimal("0.92"),
governance=GovernanceAssessment(
decision="approval_required" if approvals else "proceed",
summary="Approval required before a customer-visible price change." if approvals else "Margin remediation can proceed to the next workflow stage.",
approvals=tuple(approvals),
risks=tuple(risks),
supporting_observations=(
SupportingObservation(
id="gross-margin",
title="Current gross margin",
summary=f"Observed gross margin is {margin_pct}%.",
source_ref="cost_floor.gross_margin_pct",
value=str(margin_pct),
),
SupportingObservation(
id="cost-per-member",
title="Observed cost per member",
summary=f"Current cost per member is {cost_per_member} EUR.",
source_ref="cost_floor.cost_per_member",
value=str(cost_per_member),
),
),
),
risks=tuple(risks),
supporting_observations=(
SupportingObservation(
id="active-price",
title="Current list price",
summary=f"Current list price is {active_price} EUR.",
source_ref="value_range.current_price_eur",
value=str(active_price),
),
),
related_model_ids=((product.active_pricing_model_id,) if product is not None else ()),
)
)
if ai_spend > Decimal("0") and cost_per_member > Decimal("0"):
ai_ratio = _money((ai_spend / cost_per_member) * Decimal("100"))
if ai_ratio > Decimal("15"):
best = simulations.get("best_ltv_scenario_id") or simulations.get("best_margin_scenario_id")
recommendations.append(
SellerRecommendation(
id="usage-pricing-signal",
recommendation_type="simulation",
priority="medium",
title="AI cost becoming material",
rationale=f"AI spend is {ai_ratio}% of current cost per member.",
suggested_action=f"Evaluate hybrid model '{best}' in the simulator before customer-visible changes.",
confidence=Decimal("0.78"),
governance=GovernanceAssessment(
decision="proceed",
summary="Simulation work can proceed without approval.",
approvals=(),
risks=(
GovernanceRisk(
id="usage-forecast-uncertainty",
severity="medium",
summary="Usage-cost signals come from a small current sample.",
mitigation="Keep the next step at simulation or controlled pilot scope until more usage data is available.",
),
),
supporting_observations=(
SupportingObservation(
id="ai-ratio",
title="AI cost ratio",
summary=f"AI cost represents {ai_ratio}% of current cost per member.",
source_ref="usage.total_ai_spend_eur",
value=str(ai_ratio),
),
),
),
risks=(
GovernanceRisk(
id="pilot-scope",
severity="low",
summary="Hybrid pricing adds operational complexity before rollout automation is mature.",
mitigation="Restrict the recommendation to simulation or small pilot scope.",
),
),
supporting_observations=(
SupportingObservation(
id="best-ltv-scenario",
title="Best LTV scenario",
summary=f"Current simulator best LTV scenario is {best}.",
source_ref="pricing_simulations.best_ltv_scenario_id",
value=str(best),
),
),
related_model_ids=(str(best),) if best else (),
)
)
accepted_tuning_ids = tuple((customer_tuning or {}).get("accepted_request_ids", []))
if accepted_tuning_ids:
request = next(
(
item
for item in (customer_tuning or {}).get("requests", [])
if item.get("id") == accepted_tuning_ids[0]
),
None,
)
outcome = request.get("result", {}) if request else {}
approvals = list(publication_assessment.approvals)
risks = list(publication_assessment.risks)
if not policy.customer_visible_tuning_enabled:
approvals.append(
ApprovalRequirement(
id="customer-visible-tuning-disabled",
title="Customer-visible tuning enablement approval",
approver_role=policy.default_approver_role,
reason="The governance policy still treats customer-visible tuning as pilot-only.",
)
)
recommendations.append(
SellerRecommendation(
id="pilot-tuning-offer",
recommendation_type="model_change",
priority="medium",
title="Pilot a seller-safe tuned hybrid offer",
rationale=(
f"Request '{accepted_tuning_ids[0]}' produced an accepted tuned configuration with "
f"LTV {outcome.get('average_comparable_customer_lifetime_value')} EUR."
),
suggested_action="Use the accepted tuning result as a controlled seller-assisted offer, not a self-serve rollout.",
confidence=Decimal("0.81"),
governance=GovernanceAssessment(
decision="approval_required",
summary="Approval required before exposing tuned pricing to customers.",
approvals=tuple(approvals),
risks=tuple(risks),
supporting_observations=(
SupportingObservation(
id="accepted-tuning-request",
title="Accepted tuning request",
summary=outcome.get("explanation", "A tuned configuration passed the current solver and LTV checks."),
source_ref=f"customer_tuning.requests[{accepted_tuning_ids[0]}]",
),
),
notes=("The current policy exposes tuned pricing only through seller-assisted workflows.",),
),
risks=tuple(risks),
supporting_observations=(
SupportingObservation(
id="tuning-request-id",
title="Accepted tuning request id",
summary=f"Accepted request: {accepted_tuning_ids[0]}.",
source_ref="customer_tuning.accepted_request_ids",
),
),
related_model_ids=(request.get("model_id"),) if request else (),
related_profile_ids=(request.get("profile_id"),) if request else (),
)
)
if market_price.get("market_high_eur") and active_price < _decimal(market_price.get("market_high_eur")):
headroom = _decimal(value_range.get("aggregate_high_eur")) - active_price
if headroom > Decimal("5"):
active_experiment_count = int(policy.metadata.get("active_experiment_count", 0))
experiment_decision = "proceed" if active_experiment_count < policy.max_active_experiments else "blocked"
approvals = ()
risks = ()
if experiment_decision == "blocked":
risks = (
GovernanceRisk(
id="experiment-capacity",
severity="medium",
summary="The configured experiment capacity is exhausted.",
mitigation="Close or review existing experiments before starting another one.",
),
)
recommendations.append(
SellerRecommendation(
id="value-headroom",
recommendation_type="research",
priority="low",
title="Value headroom above list price",
rationale=f"Aggregate value band high is {value_range.get('aggregate_high_eur')} EUR vs {active_price} EUR list.",
suggested_action="Run a staged price experiment within the solo-builder segment band.",
confidence=Decimal("0.63"),
governance=GovernanceAssessment(
decision=experiment_decision,
summary=(
"Experiment capacity is available."
if experiment_decision == "proceed"
else "Blocked until experiment capacity is freed."
),
approvals=approvals,
risks=risks,
supporting_observations=(
SupportingObservation(
id="experiment-capacity",
title="Experiment capacity",
summary=f"{active_experiment_count} active experiments vs cap {policy.max_active_experiments}.",
source_ref="governance_policy.metadata.active_experiment_count",
),
),
),
risks=risks,
supporting_observations=(
SupportingObservation(
id="value-headroom-evidence",
title="Value headroom estimate",
summary=f"Observed value headroom is {headroom} EUR.",
source_ref="value_range.aggregate_high_eur",
value=str(headroom),
),
),
)
)
if provider_publication and product is not None and publication_assessment.decision != "proceed":
recommendations.append(
SellerRecommendation(
id="execution-governance-gate",
recommendation_type="execution",
priority="medium",
title="Keep provider execution behind governance gates",
rationale=publication_assessment.summary,
suggested_action="Resolve publication blockers or route the rollout through explicit approval before customer-visible execution.",
confidence=Decimal("0.88"),
governance=publication_assessment,
risks=publication_assessment.risks,
supporting_observations=publication_assessment.supporting_observations,
related_model_ids=(provider_publication.get("model_id"),),
notes=publication_assessment.notes,
)
)
if not recommendations:
recommendations.append(
SellerRecommendation(
id="hold-course",
recommendation_type="research",
priority="low",
title="Hold current pricing",
rationale="No urgent margin, usage, or competitive signals currently justify a governed change workflow.",
suggested_action="Continue observatory tracking and re-run after the next ledger period.",
confidence=Decimal("0.71"),
governance=GovernanceAssessment(
decision="proceed",
summary="No immediate governed pricing action is required.",
approvals=(),
risks=(),
supporting_observations=(),
),
risks=(),
supporting_observations=(),
)
)
return _serialize(recommendations)
def build_safe_tuning_contracts(
models: list[Any],
customer_tuning: dict[str, Any],
policy: GovernancePolicy,
active_model_id: str,
) -> list[dict[str, Any]]:
tradeoff_lexicon = {
"lower_included_usage": "Lower included usage can unlock lower variable pricing while protecting seller economics.",
"higher_included_usage": "Higher included usage increases predictability but may require stronger commitment or a higher usage price.",
"lower_usage_price": "Lower usage price is only offered when the solver can preserve seller-side LTV.",
"higher_usage_price": "Higher usage price may be the trade-off for keeping monthly access fees and flexibility unchanged.",
"longer_contract_duration": "Longer commitment can support better unit economics.",
"minimum_monthly_turnover": "Minimum turnover protects the seller against under-utilization risk.",
"prepayment": "Prepayment reduces default risk and can support better pricing.",
"guaranteed_platform_fee": "A guaranteed fee protects base platform economics.",
"customer_funded_onboarding": "Customer-funded onboarding offsets seller setup effort.",
"reduced_cancellation_flexibility": "Reduced cancellation flexibility supports stronger lifetime value assumptions.",
}
requests_by_model: dict[str, list[dict[str, Any]]] = {}
for item in customer_tuning.get("requests", []):
if item.get("model_id"):
requests_by_model.setdefault(item["model_id"], []).append(item)
contracts: list[SafeTuningContract] = []
for model in models:
tunables = [parameter for parameter in model.tunable_parameters if parameter.parameter_class == "customer_tunable"]
if not tunables:
continue
examples: list[SafeTuningExample] = []
for item in requests_by_model.get(model.id, []):
outcome = item.get("result", {})
decision = outcome.get("decision", item.get("decision", "rejected"))
customer_visible = (
policy.customer_visible_tuning_enabled
and (not policy.customer_visible_tuning_requires_active_model or model.id == active_model_id)
and decision == "accepted"
)
examples.append(
SafeTuningExample(
id=item["id"],
title=item["name"],
outcome=decision,
summary=outcome.get("explanation", "No tuning explanation available."),
customer_message=(
"This configuration is available through a seller-assisted pilot."
if decision == "accepted"
else "This configuration is outside the current safe self-serve range."
),
visible_to_customer=customer_visible,
tradeoffs=tuple(outcome.get("tradeoffs", [])),
)
)
contracts.append(
SafeTuningContract(
model_id=model.id,
model_name=model.name,
mode=(
"customer_visible"
if policy.customer_visible_tuning_enabled and model.id == active_model_id
else "pilot_only"
),
customer_visible=(
policy.customer_visible_tuning_enabled
and (not policy.customer_visible_tuning_requires_active_model or model.id == active_model_id)
),
tunable_parameters=tuple(
SafeTuningParameter(
key=parameter.key,
label=parameter.key.replace("_", " ").title(),
description=parameter.description,
data_type=parameter.data_type,
default_value=parameter.default_value,
min_value=str(parameter.min_value) if parameter.min_value is not None else None,
max_value=str(parameter.max_value) if parameter.max_value is not None else None,
)
for parameter in tunables
),
tradeoff_lexicon=tradeoff_lexicon,
examples=tuple(examples),
notes=(
"Customer-visible tuning remains policy-governed even when the solver can find a safe configuration.",
"Examples describe current pilot outcomes and should not be treated as automatically executable offers.",
),
)
)
return _serialize(contracts)
def build_governance_health_checks(
cost_floor: dict[str, Any],
customer_tuning: dict[str, Any],
provider_publication: dict[str, Any],
policy: GovernancePolicy,
) -> list[dict[str, Any]]:
checks: list[HealthCheck] = []
margin_pct = _decimal(cost_floor.get("gross_margin_pct"))
if margin_pct < Decimal("0"):
checks.append(
HealthCheck(
id="margin-health",
title="Margin health",
status="fail",
summary="Current gross margin is negative.",
value=str(margin_pct),
threshold="0",
suggested_action="Do not execute customer-visible pricing changes without correcting the current economics gap.",
)
)
elif margin_pct < Decimal("10"):
checks.append(
HealthCheck(
id="margin-health",
title="Margin health",
status="warn",
summary="Current gross margin is below the 10% operating comfort threshold.",
value=str(margin_pct),
threshold="10",
suggested_action="Prioritize simulation and model-change recommendations before expansion.",
)
)
else:
checks.append(
HealthCheck(
id="margin-health",
title="Margin health",
status="pass",
summary="Current gross margin is within the configured comfort threshold.",
value=str(margin_pct),
threshold="10",
)
)
artifact_counts = provider_publication.get("artifact_counts", {})
unsupported = int(artifact_counts.get("unsupported", 0))
drift_count = len(provider_publication.get("plan", {}).get("drift", []))
approximate = int(artifact_counts.get("approximate", 0))
if unsupported > 0 or drift_count > 0:
checks.append(
HealthCheck(
id="provider-execution-health",
title="Provider execution health",
status="fail",
summary="Stripe execution preview is not rollout-ready under the current governance policy.",
value=f"{unsupported} unsupported / {drift_count} drift",
threshold="0 unsupported / 0 drift",
suggested_action="Resolve unsupported mappings or drift before attempting execution.",
)
)
elif approximate > 0:
checks.append(
HealthCheck(
id="provider-execution-health",
title="Provider execution health",
status="warn",
summary="Stripe execution preview depends on approximate mappings.",
value=str(approximate),
threshold="0",
suggested_action="Keep rollout gated behind approval and supplemental operational controls.",
)
)
else:
checks.append(
HealthCheck(
id="provider-execution-health",
title="Provider execution health",
status="pass",
summary="Stripe execution preview is exact and drift-free.",
)
)
accepted_count = len(customer_tuning.get("accepted_request_ids", []))
checks.append(
HealthCheck(
id="tuning-pilot-health",
title="Tuning pilot health",
status="pass" if accepted_count > 0 else "warn",
summary=(
"At least one tuned offer currently passes the solver and governance pilot checks."
if accepted_count > 0
else "No tuned pilot request currently passes the solver."
),
value=str(accepted_count),
threshold=">=1 accepted pilot",
suggested_action=None if accepted_count > 0 else "Revise the pilot requests or relax no-longer-needed constraints after review.",
)
)
active_experiment_count = int(policy.metadata.get("active_experiment_count", 0))
checks.append(
HealthCheck(
id="experiment-capacity",
title="Experiment capacity",
status="pass" if active_experiment_count < policy.max_active_experiments else "warn",
summary=f"{active_experiment_count} active experiments vs cap {policy.max_active_experiments}.",
value=str(active_experiment_count),
threshold=str(policy.max_active_experiments),
suggested_action=(
None
if active_experiment_count < policy.max_active_experiments
else "Close or review an active experiment before starting another governed experiment."
),
)
)
return _serialize(checks)
def build_governance_surfaces(
data_dir: Path,
product: Any,
models: list[Any],
cost_floor: dict[str, Any],
customer_tuning: dict[str, Any],
provider_publication: dict[str, Any],
policy: GovernancePolicy,
) -> dict[str, Any]:
state = load_stripe_publication_state(default_stripe_state_path(data_dir))
publication_assessment = _publication_assessment(product, provider_publication, policy)
audit_surface = {
"provider": "stripe",
"active_revision_id": state.active_revision_id,
"active_model_id": state.active_model_id,
"revision_count": len(state.revisions),
"recent_revisions": [
{
"revision_id": revision.revision_id,
"model_id": revision.model_id,
"summary": revision.summary,
"operation_count": len(revision.operations),
"replaced_revision_id": revision.replaced_revision_id,
}
for revision in state.revisions[-5:]
],
}
return _serialize(
{
"policy": policy,
"publication_assessment": publication_assessment,
"health_checks": build_governance_health_checks(
cost_floor,
customer_tuning,
provider_publication,
policy,
),
"safe_tuning_contracts": build_safe_tuning_contracts(
models,
customer_tuning,
policy,
product.active_pricing_model_id,
),
"audit_surface": audit_surface,
"notes": [
"Governance surfaces are policy-driven and machine-readable so both humans and agents can reason about pricing changes.",
"Execution recommendations remain distinct from shadow-state publication and from customer-visible rollout approval.",
],
}
)

View File

@@ -0,0 +1 @@
"""File-based importers for Bubble, Stripe, and OpenRouter exports."""

View File

@@ -0,0 +1,13 @@
from __future__ import annotations
import json
from pathlib import Path
def read_export(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def write_registry(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")

View File

@@ -0,0 +1,55 @@
from __future__ import annotations
import argparse
from pathlib import Path
from . import _io
BUBBLE_STATUS = {"Active": "active", "Cancelled": "churned", "Paused": "paused"}
def import_membership(export: dict, plan_id: str = "flat-899-eur-monthly") -> dict:
members = []
for index, user in enumerate(export.get("users", []), start=1):
bubble_id = user.get("bubble_id") or user.get("_id") or f"bubble-{index}"
username = user.get("username") or user.get("email") or bubble_id
status = BUBBLE_STATUS.get(user.get("status", "Active"), "active")
members.append(
{
"id": f"member-{username}",
"username": username,
"external_id": bubble_id,
"status": status,
"joined_at": user.get("created") or user.get("joined_at"),
"plan_id": user.get("plan") or plan_id,
"source": "bubble",
"churned_at": user.get("cancelled_at") if status == "churned" else None,
}
)
return {
"version": 1,
"snapshot_date": export.get("exported_at", export.get("snapshot_date")),
"members": members,
"note": "Imported from Bubble export",
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Import Bubble membership export")
parser.add_argument("--input", type=Path, required=True, help="Bubble JSON export")
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).resolve().parent.parent.parent / "data" / "membership.json",
)
parser.add_argument("--plan-id", default="flat-899-eur-monthly")
args = parser.parse_args(argv)
payload = import_membership(_io.read_export(args.input), args.plan_id)
_io.write_registry(args.output, payload)
print(f"Wrote {len(payload['members'])} members → {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,57 @@
from __future__ import annotations
import argparse
from decimal import Decimal
from pathlib import Path
from . import _io
def _money(value: str | int | float) -> str:
return f"{Decimal(str(value)):.2f}"
def import_usage(export: dict, fx_usd_eur: str = "0.92") -> dict:
rate = Decimal(str(fx_usd_eur))
records = []
for index, row in enumerate(export.get("usage", export.get("records", [])), start=1):
cost_usd = Decimal(str(row.get("cost_usd") or row.get("cost", "0")))
cost_eur = (cost_usd * rate).quantize(Decimal("0.01"))
records.append(
{
"id": row.get("id") or f"usage-{row['period']}-{index}",
"period": row["period"],
"member_id": row.get("member_id") or row.get("user_id"),
"model": row["model"],
"tokens": row.get("tokens", 0),
"cost_usd": _money(cost_usd),
"cost_eur": _money(cost_eur),
"source": "openrouter",
}
)
return {
"version": 1,
"fx_usd_eur": str(rate),
"records": sorted(records, key=lambda item: (item["period"], item["id"])),
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Import OpenRouter usage export")
parser.add_argument("--input", type=Path, required=True, help="OpenRouter JSON export")
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).resolve().parent.parent.parent / "data" / "usage_records.json",
)
parser.add_argument("--fx-usd-eur", default="0.92")
args = parser.parse_args(argv)
payload = import_usage(_io.read_export(args.input), args.fx_usd_eur)
_io.write_registry(args.output, payload)
print(f"Wrote {len(payload['records'])} usage records → {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,54 @@
from __future__ import annotations
import argparse
from decimal import Decimal
from pathlib import Path
from . import _io
def _money(value: str | int | float) -> str:
return f"{Decimal(str(value)):.2f}"
def import_payments(export: dict) -> dict:
records = []
for index, charge in enumerate(export.get("charges", export.get("records", [])), start=1):
period = charge["period"]
records.append(
{
"id": charge.get("id") or f"pay-{period}-{index}",
"period": period,
"gross_amount": _money(charge.get("gross") or charge["gross_amount"]),
"fees_amount": _money(charge.get("fee") or charge.get("fees_amount", "0")),
"refunds_amount": _money(charge.get("refunds_amount", "0")),
"net_amount": _money(charge.get("net") or charge["net_amount"]),
"currency": charge.get("currency", "EUR"),
"source": "stripe",
"member_count": charge.get("member_count", 1),
"member_username": charge.get("customer") or charge.get("member_username"),
"product": charge.get("product"),
"payout_account": charge.get("payout_account"),
}
)
return {"version": 2, "records": sorted(records, key=lambda item: item["period"])}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Import Stripe charge export")
parser.add_argument("--input", type=Path, required=True, help="Stripe JSON export")
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).resolve().parent.parent.parent / "data" / "payment_records.json",
)
args = parser.parse_args(argv)
payload = import_payments(_io.read_export(args.input))
_io.write_registry(args.output, payload)
print(f"Wrote {len(payload['records'])} payment records → {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,121 @@
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from .models import (
Budget,
ExpenseRecord,
MembershipRecord,
MonthlyPlatformCost,
PaymentRecord,
)
TWOPLACES = Decimal("0.01")
def _quantize(value: Decimal) -> Decimal:
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
def convert_to_eur(amount: Decimal, currency: str, fx_rates: dict[str, Decimal]) -> Decimal:
if currency == "EUR":
return amount
if currency == "USD":
return amount * fx_rates.get("USD/EUR", Decimal("1"))
raise ValueError(f"unsupported expense currency: {currency}")
def aggregate_infrastructure_by_period(
expenses: list[ExpenseRecord],
fx_rates: dict[str, Decimal],
reporting_currency: str = "EUR",
) -> dict[str, Decimal]:
if reporting_currency != "EUR":
raise ValueError("only EUR reporting currency is supported")
totals: dict[str, Decimal] = {}
for record in expenses:
if record.cost_class != "infrastructure":
continue
totals[record.period] = totals.get(record.period, Decimal("0")) + convert_to_eur(
record.amount, record.currency, fx_rates
)
return {period: _quantize(total) for period, total in totals.items()}
def payment_processing_by_period(payments: list[PaymentRecord]) -> dict[str, Decimal]:
totals: dict[str, Decimal] = {}
for record in payments:
totals[record.period] = totals.get(record.period, Decimal("0")) + record.fees_amount
return {period: _quantize(total) for period, total in totals.items()}
def _period_sort_key(period: str) -> tuple[int, int]:
year, month = period.split("-")
return int(year), int(month)
def periods_from_budget_through_latest(
budget: Budget,
expenses: list[ExpenseRecord],
payments: list[PaymentRecord],
) -> list[str]:
candidates = {budget.started}
candidates.update(record.period for record in expenses)
candidates.update(record.period for record in payments)
latest = max(candidates, key=_period_sort_key)
periods: list[str] = []
year, month = map(int, budget.started.split("-"))
end_year, end_month = map(int, latest.split("-"))
while (year, month) <= (end_year, end_month):
periods.append(f"{year}-{month:02d}")
month += 1
if month > 12:
month = 1
year += 1
return periods
def active_members_for_period(period: str, members: list[MembershipRecord]) -> int:
period_start = f"{period}-01"
count = 0
for member in members:
if member.joined_at[:7] > period:
continue
if member.churned_at and member.churned_at[:7] < period:
continue
if member.status == "active" or (
member.churned_at and member.churned_at[:7] >= period
):
count += 1
return count
def build_monthly_ledger(
budget: Budget,
expenses: list[ExpenseRecord],
payments: list[PaymentRecord],
members: list[MembershipRecord],
fx_rates: dict[str, Decimal],
) -> list[MonthlyPlatformCost]:
infrastructure = aggregate_infrastructure_by_period(expenses, fx_rates)
processing = payment_processing_by_period(payments)
payment_by_period = {record.period: record for record in payments}
rows: list[MonthlyPlatformCost] = []
for period in periods_from_budget_through_latest(budget, expenses, payments):
payment = payment_by_period.get(period)
rows.append(
MonthlyPlatformCost(
period=period,
infrastructure_cost=infrastructure.get(period, Decimal("0.00")),
payment_processing_cost=processing.get(period, Decimal("0.00")),
active_members=(
payment.member_count
if payment and payment.member_count
else active_members_for_period(period, members)
),
gross_revenue=payment.gross_amount if payment else Decimal("0.00"),
)
)
return rows

View File

@@ -0,0 +1,162 @@
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
from ._repo_root import ensure_repo_root_on_syspath
ensure_repo_root_on_syspath()
from adaptive_pricing_core.pricing_models import load_pricing_models as load_canonical_pricing_models
from .ledger import build_monthly_ledger
from .models import (
Budget,
ExpenseRecord,
MembershipRecord,
MonthlyPlatformCost,
PaymentRecord,
PricingModel,
Product,
)
def _money(value: str | int | float) -> Decimal:
return Decimal(str(value))
def _read_json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def default_data_dir() -> Path:
return Path(__file__).resolve().parent.parent / "data"
def load_product(data_dir: Path | None = None) -> Product:
raw = _read_json((data_dir or default_data_dir()) / "product.json")
return Product(
id=raw["id"],
name=raw["name"],
lifecycle_phase=raw["lifecycle_phase"],
currency=raw["currency"],
description=raw["description"],
active_pricing_model_id=raw["active_pricing_model_id"],
)
def load_budget(data_dir: Path | None = None) -> Budget:
raw = _read_json((data_dir or default_data_dir()) / "budget.json")
return Budget(
currency=raw["currency"],
initial_budget=_money(raw["initial_budget"]),
started=raw["started"],
)
def load_pricing_models(data_dir: Path | None = None) -> list[PricingModel]:
return load_canonical_pricing_models((data_dir or default_data_dir()) / "pricing-models.json")
def load_fx_rates(data_dir: Path | None = None) -> dict[str, Decimal]:
raw = _read_json((data_dir or default_data_dir()) / "expense_records.json")
return {pair: _money(rate) for pair, rate in raw.get("fx_rates", {}).items()} if raw.get(
"fx_rates"
) else {}
def load_expense_records(data_dir: Path | None = None) -> list[ExpenseRecord]:
raw = _read_json((data_dir or default_data_dir()) / "expense_records.json")
return [
ExpenseRecord(
id=item["id"],
period=item["period"],
vendor=item["vendor"],
description=item["description"],
cost_class=item["cost_class"],
amount=_money(item["amount"]),
currency=item["currency"],
source=item["source"],
)
for item in raw["records"]
]
def load_payment_records(data_dir: Path | None = None) -> list[PaymentRecord]:
root = data_dir or default_data_dir()
path = root / "payment_records.json"
if not path.exists():
# Backward compatibility with legacy revenue.json
path = root / "revenue.json"
raw = _read_json(path)
items = raw.get("records", raw.get("entries", []))
return [
PaymentRecord(
id=item["id"],
period=item["period"],
gross_amount=_money(item["gross_amount"]),
fees_amount=_money(item["fees_amount"]),
refunds_amount=_money(item.get("refunds_amount", "0")),
net_amount=_money(item["net_amount"]),
currency=item["currency"],
source=item["source"],
member_count=item.get("member_count", 0),
member_username=item.get("member_username"),
payout_account=item.get("payout_account"),
)
for item in items
]
def load_value_range(data_dir: Path | None = None) -> dict:
return _read_json((data_dir or default_data_dir()) / "value_range.json")
def load_market_signals(data_dir: Path | None = None) -> dict:
return _read_json((data_dir or default_data_dir()) / "market_signals.json")
def load_ltv_scenarios(data_dir: Path | None = None) -> dict:
return _read_json((data_dir or default_data_dir()) / "ltv_scenarios.json")
def load_governance_policy(data_dir: Path | None = None) -> dict:
path = (data_dir or default_data_dir()) / "governance_policy.json"
if not path.exists():
return {}
return _read_json(path)
def load_tuning_requests(data_dir: Path | None = None) -> dict:
path = (data_dir or default_data_dir()) / "tuning_requests.json"
if not path.exists():
return {}
return _read_json(path)
def load_membership(data_dir: Path | None = None) -> list[MembershipRecord]:
raw = _read_json((data_dir or default_data_dir()) / "membership.json")
return [
MembershipRecord(
id=item["id"],
status=item["status"],
joined_at=item["joined_at"],
plan_id=item["plan_id"],
churned_at=item.get("churned_at"),
)
for item in raw["members"]
]
def load_monthly_ledger(data_dir: Path | None = None) -> list[MonthlyPlatformCost]:
root = data_dir or default_data_dir()
return build_monthly_ledger(
load_budget(root),
load_expense_records(root),
load_payment_records(root),
load_membership(root),
load_fx_rates(root),
)
def latest_period(monthly_costs: list[MonthlyPlatformCost]) -> str:
return max(item.period for item in monthly_costs)

View File

@@ -0,0 +1,210 @@
from __future__ import annotations
from decimal import Decimal
from typing import Any
from ._repo_root import ensure_repo_root_on_syspath
from .boundary import build_boundary_policy
from .models import EconomicsSnapshot, PricingModel
ensure_repo_root_on_syspath()
from adaptive_pricing_core.boundary_engine import PricingConfiguration # noqa: E402
from adaptive_pricing_core.comparable_ltv import ( # noqa: E402
ComparableCustomerProfile,
LTVPolicy,
SensitivityCase,
compare_pricing_configurations,
)
def _serialize(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {key: _serialize(getattr(value, key)) for key in value.__dataclass_fields__}
if isinstance(value, tuple):
return [_serialize(item) for item in value]
if isinstance(value, list):
return [_serialize(item) for item in value]
if isinstance(value, dict):
return {key: _serialize(item) for key, item in value.items()}
return value
def _decimal(value: Decimal | str | int | float | None) -> Decimal:
if value in (None, ""):
return Decimal("0")
return Decimal(str(value))
def _usage_component(model: PricingModel):
return next((component for component in model.charge_components if component.kind == "usage"), None)
def _included_units(model: PricingModel, members_per_customer: int) -> Decimal | None:
usage = _usage_component(model)
if not usage or usage.included_units is None:
return None
return usage.included_units * Decimal(members_per_customer)
def _usage_unit_price(model: PricingModel) -> Decimal | None:
usage = _usage_component(model)
if not usage or usage.unit_price is None:
return None
return usage.unit_price
def _usage_unit_cost(records: list[dict[str, Any]], period: str) -> Decimal:
period_rows = [row for row in records if row.get("period") == period]
total_units = sum(_decimal(row.get("tokens")) for row in period_rows)
total_cost = sum(_decimal(row.get("cost_eur")) for row in period_rows)
if total_units <= Decimal("0"):
return Decimal("0")
return total_cost / total_units
def _payment_fee_rate(snapshot: EconomicsSnapshot) -> Decimal:
if snapshot.monthly_revenue <= Decimal("0"):
return Decimal("0")
return (snapshot.monthly_payment_processing_cost / snapshot.monthly_revenue) * Decimal("100")
def _profile(raw: dict[str, Any]) -> ComparableCustomerProfile:
return ComparableCustomerProfile(
id=raw["id"],
name=raw["name"],
segment=raw["segment"],
eligible_model_ids=tuple(raw.get("eligible_model_ids", [])),
members_per_customer=int(raw.get("members_per_customer", 1)),
expected_monthly_usage_units=_decimal(raw.get("expected_monthly_usage_units")),
usage_variance_pct=_decimal(raw.get("usage_variance_pct")),
monthly_churn_pct=_decimal(raw.get("monthly_churn_pct")),
monthly_default_pct=_decimal(raw.get("monthly_default_pct")),
monthly_support_cost=_decimal(raw.get("monthly_support_cost")),
monthly_risk_cost=_decimal(raw.get("monthly_risk_cost")),
acquisition_cost=_decimal(raw.get("acquisition_cost")),
upfront_investment_cost=_decimal(raw.get("upfront_investment_cost")),
allocated_fixed_cost=_decimal(raw["allocated_fixed_cost"]) if raw.get("allocated_fixed_cost") else None,
notes=raw.get("notes", ""),
)
def _sensitivity_case(raw: dict[str, Any]) -> SensitivityCase:
return SensitivityCase(
id=raw["id"],
name=raw["name"],
usage_multiplier=_decimal(raw.get("usage_multiplier", "1")),
usage_variance_delta_pct=_decimal(raw.get("usage_variance_delta_pct")),
monthly_churn_delta_pct=_decimal(raw.get("monthly_churn_delta_pct")),
monthly_default_delta_pct=_decimal(raw.get("monthly_default_delta_pct")),
monthly_support_cost_delta=_decimal(raw.get("monthly_support_cost_delta")),
monthly_risk_cost_delta=_decimal(raw.get("monthly_risk_cost_delta")),
)
def _ltv_policy(raw: dict[str, Any]) -> LTVPolicy:
return LTVPolicy(
horizon_months=int(raw.get("horizon_months", 24)),
monthly_discount_rate_pct=_decimal(raw.get("monthly_discount_rate_pct", "1.0")),
required_improvement_factor=_decimal(raw.get("required_improvement_factor", "1.05")),
)
def _configuration(
model: PricingModel,
profile: ComparableCustomerProfile,
snapshot: EconomicsSnapshot,
usage_unit_cost: Decimal,
) -> PricingConfiguration:
members_per_customer = max(profile.members_per_customer, 1)
per_member_fixed_cost = (
snapshot.monthly_infrastructure_cost / snapshot.active_members
if snapshot.active_members
else snapshot.monthly_infrastructure_cost
)
allocated_fixed_cost = (
profile.allocated_fixed_cost
if profile.allocated_fixed_cost is not None
else per_member_fixed_cost * Decimal(members_per_customer)
)
return PricingConfiguration(
model=model,
segment=profile.segment,
expected_usage_units=profile.expected_monthly_usage_units,
expected_usage_variance_pct=profile.usage_variance_pct,
allocated_fixed_cost=allocated_fixed_cost,
unit_cost=usage_unit_cost,
support_cost=profile.monthly_support_cost,
risk_cost=profile.monthly_risk_cost,
payment_fee_rate_pct=_payment_fee_rate(snapshot),
access_fee_amount=model.access_fee_amount * Decimal(members_per_customer),
included_units=_included_units(model, members_per_customer),
usage_unit_price=_usage_unit_price(model),
)
def build_ltv_simulations(
snapshot: EconomicsSnapshot,
models: list[PricingModel],
usage_records: list[dict[str, Any]],
scenario_catalog: dict[str, Any],
) -> dict[str, Any]:
policy = _ltv_policy(scenario_catalog)
boundary_policy = build_boundary_policy(snapshot)
sensitivity_cases = tuple(_sensitivity_case(item) for item in scenario_catalog.get("sensitivity_cases", []))
observed_usage_unit_cost = _usage_unit_cost(usage_records, snapshot.period)
profile_results = []
for raw_profile in scenario_catalog.get("profiles", []):
profile = _profile(raw_profile)
configurations = [
_configuration(model, profile, snapshot, observed_usage_unit_cost)
for model in models
if model.status in ("active", "candidate")
]
profile_results.append(
compare_pricing_configurations(
configurations,
profile,
boundary_policy,
policy,
sensitivity_cases=sensitivity_cases,
)
)
primary = profile_results[0] if profile_results else None
primary_scenarios = list(primary.comparisons) if primary else []
active_model = next((model for model in models if model.status == "active"), None)
best_margin = max(primary_scenarios, key=lambda item: item.base_monthly_margin, default=None)
best_ltv = max(
primary_scenarios,
key=lambda item: item.average_comparable_customer_lifetime_value,
default=None,
)
return _serialize({
"period": snapshot.period,
"currency": snapshot.currency,
"required_improvement_factor": policy.required_improvement_factor,
"horizon_months": policy.horizon_months,
"monthly_discount_rate_pct": policy.monthly_discount_rate_pct,
"active_scenario_id": active_model.id if active_model else None,
"best_margin_scenario_id": best_margin.model_id if best_margin else None,
"best_ltv_scenario_id": best_ltv.model_id if best_ltv else None,
"reference_model_id": primary.reference_model_id if primary else None,
"primary_profile_id": primary.profile.id if primary else None,
"scenarios": primary_scenarios,
"profile_comparisons": profile_results,
"calibration": {
"observed_usage_unit_cost": observed_usage_unit_cost,
"observed_payment_fee_rate_pct": _payment_fee_rate(snapshot),
"profile_count": len(profile_results),
},
"notes": [
scenario_catalog.get("notes", ""),
"Primary scenarios expose the first configured comparable-customer profile for backward-compatible UI consumers.",
"Profile comparisons compare candidate models using discounted seller LTV rather than only current-period gross margin.",
],
})

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
from .models import MembershipRecord
def _period_prefix(date: str | None) -> str | None:
if not date or len(date) < 7:
return None
return date[:7]
def build_membership_analytics(
members: list[MembershipRecord],
period: str,
history: list[str],
) -> dict:
active = sum(1 for member in members if member.status == "active")
new_members = sum(1 for member in members if _period_prefix(member.joined_at) == period)
churned = sum(1 for member in members if _period_prefix(member.churned_at) == period)
prior_period = None
sorted_periods = sorted(history)
if period in sorted_periods:
index = sorted_periods.index(period)
if index > 0:
prior_period = sorted_periods[index - 1]
prior_active = active
if prior_period:
prior_active = sum(
1
for member in members
if member.status == "active" and _period_prefix(member.joined_at) <= prior_period
)
growth_rate = None
if prior_active:
growth_rate = round(((active - prior_active) / prior_active) * 100, 1)
return {
"period": period,
"total_members": len(members),
"active_members": active,
"new_members": new_members,
"churned_members": churned,
"growth_rate_pct": growth_rate,
"snapshot_source": "membership.json",
}

View File

@@ -0,0 +1,120 @@
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from typing import Literal
from ._repo_root import ensure_repo_root_on_syspath
ensure_repo_root_on_syspath()
from adaptive_pricing_core.pricing_models import ( # noqa: E402
ChargeComponent,
Commitment,
PricingModel,
PricingModelStatus,
TunableParameter,
)
ExpenseClass = Literal["infrastructure", "payment_processing"]
MemberStatus = Literal["active", "churned", "paused"]
LiquidityStatus = Literal["burning", "neutral", "generating"]
@dataclass(frozen=True)
class Product:
id: str
name: str
lifecycle_phase: str
currency: str
description: str
active_pricing_model_id: str
@dataclass(frozen=True)
class ExpenseRecord:
id: str
period: str
vendor: str
description: str
cost_class: ExpenseClass
amount: Decimal
currency: str
source: str
@dataclass(frozen=True)
class PaymentRecord:
id: str
period: str
gross_amount: Decimal
fees_amount: Decimal
refunds_amount: Decimal
net_amount: Decimal
currency: str
source: str
member_count: int = 0
member_username: str | None = None
payout_account: str | None = None
@dataclass(frozen=True)
class MonthlyPlatformCost:
period: str
infrastructure_cost: Decimal
payment_processing_cost: Decimal
active_members: int
gross_revenue: Decimal
@property
def total_platform_cost(self) -> Decimal:
return self.infrastructure_cost + self.payment_processing_cost
@dataclass(frozen=True)
class Budget:
currency: str
initial_budget: Decimal
started: str
@dataclass(frozen=True)
class MembershipRecord:
id: str
status: MemberStatus
joined_at: str
plan_id: str
churned_at: str | None = None
@dataclass(frozen=True)
class EconomicsSnapshot:
period: str
currency: str
active_members: int
monthly_revenue: Decimal
monthly_infrastructure_cost: Decimal
monthly_payment_processing_cost: Decimal
monthly_total_platform_cost: Decimal
cost_per_member: Decimal
gross_margin: Decimal
gross_margin_pct: Decimal
pricing_model_count: int
revenue_source: str
period_net_liquidity: Decimal
liquidity_status: LiquidityStatus
@dataclass(frozen=True)
class LiquiditySummary:
currency: str
through_period: str
initial_budget: Decimal
cumulative_member_payments: Decimal
cumulative_infrastructure_cost: Decimal
cumulative_payment_processing_cost: Decimal
cumulative_total_platform_cost: Decimal
cumulative_net_liquidity: Decimal
remaining_budget: Decimal
liquidity_status: LiquidityStatus
months_tracked: int

View File

@@ -0,0 +1,93 @@
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from .economics import active_pricing_model
from .models import EconomicsSnapshot, PricingModel, Product
TWOPLACES = Decimal("0.01")
def _quantize(value: Decimal) -> Decimal:
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
def build_cost_floor(
snapshot: EconomicsSnapshot,
models: list[PricingModel],
) -> dict:
active = next((m for m in models if m.status == "active"), None)
return {
"period": snapshot.period,
"currency": snapshot.currency,
"monthly_infrastructure_cost": snapshot.monthly_infrastructure_cost,
"monthly_payment_processing_cost": snapshot.monthly_payment_processing_cost,
"monthly_total_platform_cost": snapshot.monthly_total_platform_cost,
"cost_per_member": snapshot.cost_per_member,
"active_members": snapshot.active_members,
"monthly_revenue": snapshot.monthly_revenue,
"gross_margin": snapshot.gross_margin,
"gross_margin_pct": snapshot.gross_margin_pct,
"active_price": active.access_fee_amount if active else Decimal("0"),
"active_model_id": active.id if active else None,
"active_model_name": active.name if active else None,
}
def build_value_range_view(
raw: dict,
snapshot: EconomicsSnapshot,
product: Product,
models: list[PricingModel],
) -> dict:
model = active_pricing_model(models, product)
current = model.access_fee_amount
segments = []
lows: list[Decimal] = []
highs: list[Decimal] = []
for item in raw.get("segments", []):
low = Decimal(str(item["low_eur"]))
high = Decimal(str(item["high_eur"]))
lows.append(low)
highs.append(high)
segments.append(
{
**item,
"headroom_to_high_eur": _quantize(high - current),
"below_floor": current < low,
}
)
aggregate_low = min(lows) if lows else current
aggregate_high = max(highs) if highs else current
return {
"currency": raw.get("currency", snapshot.currency),
"product_id": raw.get("product_id", product.id),
"current_price_eur": current,
"aggregate_low_eur": aggregate_low,
"aggregate_high_eur": aggregate_high,
"cost_per_member_eur": snapshot.cost_per_member,
"segments": segments,
"value_drivers": raw.get("value_drivers", []),
"notes": raw.get("notes", ""),
}
def build_market_price_view(raw: dict) -> dict:
alternatives = list(raw.get("alternatives", []))
prices = [
Decimal(str(item["price_monthly_eur"]))
for item in alternatives
if item.get("price_monthly_eur") not in (None, "", "0", "0.00")
]
return {
"currency": raw.get("currency", "EUR"),
"last_reviewed": raw.get("last_reviewed"),
"alternative_count": len(alternatives),
"priced_alternative_count": len(prices),
"market_low_eur": min(prices) if prices else None,
"market_high_eur": max(prices) if prices else None,
"alternatives": alternatives,
"notes": raw.get("notes", ""),
}

View File

@@ -0,0 +1,159 @@
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
from typing import Any
from ._repo_root import ensure_repo_root_on_syspath
from .models import PricingModel, Product
ensure_repo_root_on_syspath()
from adaptive_pricing_core.provider_publication import ( # noqa: E402
CatalogProduct,
ProviderPublicationState,
apply_publication,
build_publication_bundle,
plan_publication,
provider_state_from_dict,
provider_state_to_dict,
rollback_publication,
)
from adaptive_pricing_core.stripe_provider import map_bundle_to_stripe # noqa: E402
def _serialize(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {key: _serialize(getattr(value, key)) for key in value.__dataclass_fields__}
if isinstance(value, tuple):
return [_serialize(item) for item in value]
if isinstance(value, list):
return [_serialize(item) for item in value]
if isinstance(value, dict):
return {key: _serialize(item) for key, item in value.items()}
return value
def default_stripe_state_path(data_dir: Path) -> Path:
return data_dir / "provider_state" / "stripe-publication.json"
def load_stripe_publication_state(path: Path) -> ProviderPublicationState:
if not path.exists():
return ProviderPublicationState(provider="stripe")
return provider_state_from_dict(json.loads(path.read_text(encoding="utf-8")))
def save_stripe_publication_state(path: Path, state: ProviderPublicationState) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(provider_state_to_dict(state), indent=2),
encoding="utf-8",
)
def _catalog_product(product: Product) -> CatalogProduct:
return CatalogProduct(
id=product.id,
name=product.name,
description=product.description,
currency=product.currency,
lifecycle_phase=product.lifecycle_phase,
active_pricing_model_id=product.active_pricing_model_id,
metadata={"product_channel": "membership"},
)
def _target_model(
models: list[PricingModel],
product: Product,
model_id: str | None = None,
) -> PricingModel:
requested_id = model_id or product.active_pricing_model_id
return next(item for item in models if item.id == requested_id)
def build_stripe_publication_preview(
product: Product,
models: list[PricingModel],
data_dir: Path,
*,
model_id: str | None = None,
state_path: Path | None = None,
) -> dict[str, Any]:
model = _target_model(models, product, model_id)
bundle = build_publication_bundle(_catalog_product(product), model)
package = map_bundle_to_stripe(bundle)
state = load_stripe_publication_state(state_path or default_stripe_state_path(data_dir))
plan = plan_publication(package, state)
return _serialize(
{
"provider": "stripe",
"state_path": str(state_path or default_stripe_state_path(data_dir)),
"model_id": model.id,
"model_name": model.name,
"current_state": {
"active_revision_id": state.active_revision_id,
"active_model_id": state.active_model_id,
"artifact_count": len(state.artifacts),
"revision_count": len(state.revisions),
},
"artifact_counts": {
"exact": sum(item.mapping_status == "exact" for item in package.artifacts),
"approximate": sum(item.mapping_status == "approximate" for item in package.artifacts),
"unsupported": sum(item.mapping_status == "unsupported" for item in package.artifacts),
},
"plan": plan,
"notes": package.notes,
}
)
def publish_to_stripe_shadow(
product: Product,
models: list[PricingModel],
data_dir: Path,
*,
model_id: str | None = None,
state_path: Path | None = None,
) -> dict[str, Any]:
path = state_path or default_stripe_state_path(data_dir)
model = _target_model(models, product, model_id)
bundle = build_publication_bundle(_catalog_product(product), model)
package = map_bundle_to_stripe(bundle)
current_state = load_stripe_publication_state(path)
result = apply_publication(package, current_state)
save_stripe_publication_state(path, result.state)
return _serialize(
{
"provider": "stripe",
"state_path": str(path),
"model_id": model.id,
"model_name": model.name,
"result": result,
}
)
def rollback_stripe_shadow(
data_dir: Path,
revision_id: str,
*,
state_path: Path | None = None,
) -> dict[str, Any]:
path = state_path or default_stripe_state_path(data_dir)
current_state = load_stripe_publication_state(path)
result = rollback_publication(current_state, revision_id)
save_stripe_publication_state(path, result.state)
return _serialize(
{
"provider": "stripe",
"state_path": str(path),
"result": result,
}
)

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
from .load import default_data_dir, load_pricing_models, load_product
from .publication import (
build_stripe_publication_preview,
publish_to_stripe_shadow,
rollback_stripe_shadow,
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Preview or apply Stripe publication for pricing models")
parser.add_argument("--data-dir", type=Path, default=default_data_dir())
parser.add_argument("--model-id", help="Pricing model id to preview or publish")
parser.add_argument("--provider", default="stripe", choices=["stripe"])
parser.add_argument("--state-path", type=Path, help="Override provider shadow-state path")
parser.add_argument("--apply", action="store_true", help="Apply the publication plan to the local Stripe shadow state")
parser.add_argument("--rollback", help="Rollback the local Stripe shadow state to a prior revision id")
args = parser.parse_args(argv)
product = load_product(args.data_dir)
models = load_pricing_models(args.data_dir)
if args.rollback:
payload = rollback_stripe_shadow(args.data_dir, args.rollback, state_path=args.state_path)
elif args.apply:
payload = publish_to_stripe_shadow(
product,
models,
args.data_dir,
model_id=args.model_id,
state_path=args.state_path,
)
else:
payload = build_stripe_publication_preview(
product,
models,
args.data_dir,
model_id=args.model_id,
state_path=args.state_path,
)
print(json.dumps(payload, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from typing import Any
from .governance import build_pricing_recommendation_workflow, build_governance_policy
def build_pricing_recommendations(
cost_floor: dict[str, Any],
value_range: dict[str, Any],
market_price: dict[str, Any],
simulations: dict[str, Any],
usage_summary: dict[str, Any],
*,
boundary_validation: dict[str, Any] | None = None,
customer_tuning: dict[str, Any] | None = None,
provider_publication: dict[str, Any] | None = None,
governance_policy: dict[str, Any] | None = None,
product: Any | None = None,
) -> list[dict[str, Any]]:
policy = build_governance_policy(governance_policy or {})
return build_pricing_recommendation_workflow(
cost_floor,
value_range,
market_price,
simulations,
usage_summary,
boundary_validation=boundary_validation,
customer_tuning=customer_tuning or {},
provider_publication=provider_publication or {},
governance_policy=policy,
product=product,
)

View File

@@ -0,0 +1,81 @@
from __future__ import annotations
import argparse
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse
from .api import build_dashboard_payload
ROOT = Path(__file__).resolve().parent.parent
UI_DIR = ROOT / "ui"
UI_CONTENT_TYPES = {
".css": "text/css",
".js": "application/javascript",
".json": "application/json",
".html": "text/html; charset=utf-8",
}
class ObservatoryHandler(BaseHTTPRequestHandler):
data_dir: Path = ROOT / "data"
def _send(self, status: int, body: bytes, content_type: str) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None:
parsed = urlparse(self.path)
if parsed.path == "/api/dashboard":
query = parse_qs(parsed.query)
period = query.get("period", [None])[0]
payload = build_dashboard_payload(self.data_dir, period)
self._send(200, json.dumps(payload).encode("utf-8"), "application/json")
return
if parsed.path == "/":
return self._serve_file(UI_DIR / "index.html", "text/html; charset=utf-8")
if parsed.path.startswith("/ui/"):
relative = parsed.path.removeprefix("/ui/")
target = UI_DIR / relative
if target.exists() and target.is_file():
content_type = UI_CONTENT_TYPES.get(target.suffix, "application/octet-stream")
if "charset" not in content_type:
content_type = f"{content_type}; charset=utf-8"
return self._serve_file(target, content_type)
self._send(404, b"Not found", "text/plain")
def _serve_file(self, path: Path, content_type: str) -> None:
self._send(200, path.read_bytes(), content_type)
def log_message(self, format: str, *args) -> None:
return
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Coulomb Economic Observatory UI server")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8765)
parser.add_argument("--data-dir", type=Path, default=ROOT / "data")
args = parser.parse_args(argv)
ObservatoryHandler.data_dir = args.data_dir
server = ThreadingHTTPServer((args.host, args.port), ObservatoryHandler)
print(f"Economic Observatory UI: http://{args.host}:{args.port}/")
print(f"API: http://{args.host}:{args.port}/api/dashboard")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopped.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
from decimal import Decimal
from typing import Any
from .ltv import build_ltv_simulations
from .models import EconomicsSnapshot, PricingModel
def _fallback_catalog(models: list[PricingModel]) -> dict[str, Any]:
return {
"version": 1,
"currency": "EUR",
"horizon_months": 24,
"monthly_discount_rate_pct": "1.0",
"required_improvement_factor": "1.05",
"profiles": [
{
"id": "observatory-default",
"name": "Observatory default",
"segment": "coulomb-social-members",
"eligible_model_ids": [model.id for model in models if model.status in ("active", "candidate")],
"members_per_customer": 1,
"expected_monthly_usage_units": "120000",
"usage_variance_pct": "25",
"monthly_churn_pct": "5.0",
"monthly_default_pct": "1.0",
"monthly_support_cost": "0.00",
"monthly_risk_cost": "0.00",
"acquisition_cost": "0.00",
"upfront_investment_cost": "0.00",
"notes": "Fallback scenario when no explicit LTV scenario catalog is provided."
}
],
"notes": "Fallback scenario catalog generated inside observatory.simulator.",
}
def _fallback_usage_records(snapshot: EconomicsSnapshot, ai_cost_per_member: Decimal) -> list[dict[str, Any]]:
return [
{
"id": "fallback-usage",
"period": snapshot.period,
"member_id": "fallback",
"tokens": 120000,
"cost_eur": ai_cost_per_member,
"source": "fallback",
}
]
def build_pricing_simulations(
snapshot: EconomicsSnapshot,
models: list[PricingModel],
ai_cost_per_member: Decimal,
usage_records: list[dict[str, Any]] | None = None,
scenario_catalog: dict[str, Any] | None = None,
) -> dict[str, Any]:
return build_ltv_simulations(
snapshot,
models,
usage_records or _fallback_usage_records(snapshot, ai_cost_per_member),
scenario_catalog or _fallback_catalog(models),
)

View File

@@ -0,0 +1,178 @@
from __future__ import annotations
from decimal import Decimal
from typing import Any
from ._repo_root import ensure_repo_root_on_syspath
from .boundary import build_boundary_policy
from .ltv import _configuration, _ltv_policy, _profile, _usage_unit_cost
from .models import EconomicsSnapshot, PricingModel
ensure_repo_root_on_syspath()
from adaptive_pricing_core.customer_tuning import ( # noqa: E402
CustomerTuningRequest,
UsagePriceSearchPolicy,
solve_customer_tuning,
)
def _serialize(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {key: _serialize(getattr(value, key)) for key in value.__dataclass_fields__}
if isinstance(value, tuple):
return [_serialize(item) for item in value]
if isinstance(value, list):
return [_serialize(item) for item in value]
if isinstance(value, dict):
return {key: _serialize(item) for key, item in value.items()}
return value
def _decimal(value: Decimal | str | int | float | None) -> Decimal | None:
if value in (None, ""):
return None
return Decimal(str(value))
def _customer_tunable_keys(model: PricingModel) -> set[str]:
return {
parameter.key
for parameter in model.tunable_parameters
if parameter.parameter_class == "customer_tunable"
}
def _validate_request_surface(model: PricingModel, selected_tunables: dict[str, Any]) -> tuple[str, ...]:
tunable_keys = _customer_tunable_keys(model)
issues: list[str] = []
for key in selected_tunables:
if key not in tunable_keys:
issues.append(f"{key} is not customer-tunable on {model.id}")
return tuple(issues)
def _request(raw: dict[str, Any]) -> CustomerTuningRequest:
selected = raw.get("selected_tunables", {})
return CustomerTuningRequest(
included_units=_decimal(selected.get("included_tokens")),
contract_duration_months=(
int(selected["contract_duration_months"])
if selected.get("contract_duration_months") is not None
else None
),
minimum_monthly_turnover=_decimal(selected.get("minimum_monthly_turnover")) or Decimal("0"),
prepaid_amount=_decimal(selected.get("prepaid_amount")) or Decimal("0"),
guaranteed_platform_fee=_decimal(selected.get("guaranteed_platform_fee")) or Decimal("0"),
customer_funded_onboarding=_decimal(selected.get("customer_funded_onboarding")) or Decimal("0"),
reduced_cancellation_flexibility=raw.get("reduced_cancellation_flexibility"),
preference=raw.get("preference", "lower_usage_price"),
approval_mode=raw.get("approval_mode", "self_serve_only"),
)
def _search_policy(raw: dict[str, Any]) -> UsagePriceSearchPolicy | None:
if not raw:
return None
return UsagePriceSearchPolicy(
min_usage_unit_price=_decimal(raw.get("min_usage_unit_price")),
max_usage_unit_price=_decimal(raw.get("max_usage_unit_price")),
usage_unit_price_step=_decimal(raw.get("usage_unit_price_step")) or Decimal("0.0001"),
max_usage_price_multiplier=_decimal(raw.get("max_usage_price_multiplier")) or Decimal("4"),
)
def build_customer_tuning_pilot(
snapshot: EconomicsSnapshot,
models: list[PricingModel],
usage_records: list[dict[str, Any]],
scenario_catalog: dict[str, Any],
request_catalog: dict[str, Any] | None = None,
) -> dict[str, Any]:
request_catalog = request_catalog or {}
if not request_catalog.get("requests"):
return {
"period": snapshot.period,
"currency": snapshot.currency,
"requests": [],
"notes": [
"No customer-tuning pilot requests are configured for this observatory deployment.",
],
}
profile_index = {item["id"]: _profile(item) for item in scenario_catalog.get("profiles", [])}
model_index = {model.id: model for model in models if model.status in ("active", "candidate")}
policy = _ltv_policy(scenario_catalog)
boundary_policy = build_boundary_policy(snapshot)
observed_usage_unit_cost = _usage_unit_cost(usage_records, snapshot.period)
results = []
for raw_request in request_catalog.get("requests", []):
model = model_index[raw_request["model_id"]]
profile = profile_index[raw_request["profile_id"]]
selected_tunables = raw_request.get("selected_tunables", {})
issues = _validate_request_surface(model, selected_tunables)
if issues:
results.append(
{
"id": raw_request["id"],
"name": raw_request["name"],
"decision": "rejected",
"issues": list(issues),
"profile_id": profile.id,
"model_id": model.id,
}
)
continue
base_configuration = _configuration(model, profile, snapshot, observed_usage_unit_cost)
reference_configurations = [
_configuration(candidate, profile, snapshot, observed_usage_unit_cost)
for candidate in models
if candidate.status in ("active", "candidate")
]
outcome = solve_customer_tuning(
base_configuration,
reference_configurations,
profile,
boundary_policy,
policy,
_request(raw_request),
search_policy=_search_policy(raw_request.get("search_policy", {})),
)
results.append(
{
"id": raw_request["id"],
"name": raw_request["name"],
"profile_id": profile.id,
"profile_name": profile.name,
"model_id": model.id,
"model_name": model.name,
"selected_tunables": selected_tunables,
"result": outcome,
}
)
accepted = [
item["id"]
for item in results
if item.get("result") is not None and getattr(item["result"], "decision", None) == "accepted"
]
return _serialize(
{
"period": snapshot.period,
"currency": snapshot.currency,
"request_count": len(results),
"accepted_request_ids": accepted,
"requests": results,
"notes": [
request_catalog.get("notes", ""),
"Pilot requests map product-level tunables into canonical pricing configuration fields before running the generic solver.",
"For Coulomb's hybrid prototype, selected included token values are treated as total package allowances rather than per-seat multipliers.",
],
}
)

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from typing import Any
TWOPLACES = Decimal("0.01")
def _money(value: Decimal) -> Decimal:
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
def load_usage_records(data_dir) -> list[dict[str, Any]]:
from pathlib import Path
from .load import _read_json, default_data_dir
root = data_dir or default_data_dir()
path = Path(root) / "usage_records.json"
if not path.exists():
return []
raw = _read_json(path)
return list(raw.get("records", []))
def build_usage_summary(records: list[dict[str, Any]], period: str) -> dict:
period_rows = [row for row in records if row.get("period") == period]
by_member: dict[str, Decimal] = {}
by_model: dict[str, Decimal] = {}
total = Decimal("0")
for row in period_rows:
cost = Decimal(str(row.get("cost_eur", "0")))
total += cost
member_id = row.get("member_id") or "unknown"
model = row.get("model") or "unknown"
by_member[member_id] = by_member.get(member_id, Decimal("0")) + cost
by_model[model] = by_model.get(model, Decimal("0")) + cost
active_members = len(by_member) or 1
return {
"period": period,
"total_ai_spend_eur": _money(total),
"cost_per_active_user_eur": _money(total / active_members),
"by_member": {key: _money(value) for key, value in sorted(by_member.items())},
"by_model": {key: _money(value) for key, value in sorted(by_model.items())},
"record_count": len(period_rows),
}

View File

@@ -0,0 +1,81 @@
# Economics Dashboard v1 — Coulomb Social Membership
**Period:** 2026-06
**Lifecycle phase:** growth
**Active pricing model:** Standard Membership (8.99 EUR/monthly)
> Platform costs accrue to the operator. Customer cost-pass-through billing is
> **not active** in MVP — members pay subscription only. All totals are computed
> programmatically from expense and payment record ledgers (58 expense
> records).
## Key Metrics (current period)
| Metric | Value |
|--------|------:|
| Active members | 1 |
| Member payments (gross) | 8.99 EUR |
| Infrastructure cost | 29.73 EUR |
| Payment processing cost | 0.44 EUR |
| Total platform cost | 30.17 EUR |
| Platform cost per member | 30.17 EUR |
| Period gross margin | -21.18 EUR |
| Period gross margin % | -235.6% |
| Period net liquidity | -21.18 EUR (burning) |
_Period net liquidity = net member payments infrastructure cost (processing fees already netted from payments)._
_Revenue source: stripe_
## Liquidity & Budget (through 2026-06)
| Metric | Value |
|--------|------:|
| Initial budget | 1000.00 EUR |
| Cumulative member payments (net) | 68.40 EUR |
| Cumulative infrastructure cost | 409.28 EUR |
| Cumulative payment processing | 3.52 EUR |
| Cumulative total platform cost | 412.80 EUR |
| Cumulative net liquidity | -340.88 EUR (burning) |
| Remaining budget | 659.12 EUR (within budget) |
| Months tracked | 18 |
## Monthly History
| Period | Members | Gross revenue | Infrastructure | Processing | Total platform | Net liquidity |
|--------|--------:|--------------:|---------------:|-----------:|---------------:|--------------:|
| 2025-01 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-02 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-03 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-04 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-05 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-06 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-07 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-08 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-09 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-10 | 0 | 0.00 | 20.74 | 0.00 | 20.74 | -20.74 |
| 2025-11 | 1 | 8.99 | 20.74 | 0.44 | 21.18 | -12.19 |
| 2025-12 | 1 | 8.99 | 20.74 | 0.44 | 21.18 | -12.19 |
| 2026-01 | 1 | 8.99 | 20.74 | 0.44 | 21.18 | -12.19 |
| 2026-02 | 1 | 8.99 | 20.74 | 0.44 | 21.18 | -12.19 |
| 2026-03 | 1 | 8.99 | 29.73 | 0.44 | 30.17 | -21.18 |
| 2026-04 | 1 | 8.99 | 29.73 | 0.44 | 30.17 | -21.18 |
| 2026-05 | 1 | 8.99 | 29.73 | 0.44 | 30.17 | -21.18 |
| 2026-06 | 1 | 8.99 | 29.73 | 0.44 | 30.17 | -21.18 |
## Pricing Model Registry
| ID | Name | Type | Status |
|----|------|------|--------|
| flat-899-eur-monthly | Standard Membership | flat_subscription | active |
| membership-plus-credits | Membership + AI Credits | hybrid_subscription_usage | candidate |
| membership-plus-overage | Membership + Overage | hybrid_subscription_usage | candidate |
## Registries Loaded
- Product model (`data/product.json`)
- Budget (`data/budget.json`)
- Expense records (`data/expense_records.json`) — source of truth for costs
- Infrastructure catalog (`data/infrastructure/`) — domain, VPS, and Stripe reference data
- Payment records (`data/payment_records.json`)
- Membership (`data/membership.json`)
- Requirements (`REQUIREMENTS.md`)

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Synchronises the vendored copy of whynot-design from a pinned upstream commit.
# Source: ~/whynot-design (worktree) or a clone from gitea.
#
# Usage: ./scripts/sync-whynot-design.sh [<commit-or-ref>]
# Default: reads .whynot-design-ref from the vendor directory, else HEAD.
#
# Pulls Layer 1 (tokens + CSS) and Layer 2 (Lit web components) so the
# observatory UI picks up design-system changes on re-run.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENDOR_DIR="$ROOT/ui/vendor/whynot-design"
REF_FILE="$VENDOR_DIR/.whynot-design-ref"
SRC_REPO="${WHYNOT_DESIGN_SRC:-$HOME/whynot-design}"
REF="${1:-}"
if [[ -z "$REF" && -f "$REF_FILE" ]]; then
REF="$(cat "$REF_FILE")"
fi
if [[ -z "$REF" ]]; then
REF="HEAD"
fi
if [[ ! -d "$SRC_REPO/.git" ]]; then
echo "Source not found: $SRC_REPO" >&2
echo "Set WHYNOT_DESIGN_SRC or clone gitea:whynot/whynot-design there." >&2
exit 1
fi
mkdir -p "$VENDOR_DIR/tokens" "$VENDOR_DIR/elements"
git -C "$SRC_REPO" show "$REF:src/styles/colors_and_type.css" > "$VENDOR_DIR/colors_and_type.css"
git -C "$SRC_REPO" show "$REF:src/styles/components.css" > "$VENDOR_DIR/components.css"
git -C "$SRC_REPO" show "$REF:src/index.js" > "$VENDOR_DIR/index.js"
for f in atoms.js chrome.js form.js icons.js layout.js _styles.js; do
git -C "$SRC_REPO" show "$REF:src/elements/$f" > "$VENDOR_DIR/elements/$f"
done
for f in colors.json type.json spacing.json index.json; do
git -C "$SRC_REPO" show "$REF:tokens/$f" > "$VENDOR_DIR/tokens/$f"
done
git -C "$SRC_REPO" rev-parse "$REF" > "$REF_FILE"
echo "Vendor synced → $VENDOR_DIR (ref: $(cat "$REF_FILE"))"

View File

@@ -0,0 +1,10 @@
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
REPO_ROOT = ROOT.parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))

View File

@@ -0,0 +1,51 @@
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
from observatory.api import build_dashboard_payload, payload_json
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def test_dashboard_payload_contains_live_ledger_totals() -> None:
payload = build_dashboard_payload(DATA_DIR, "2026-06")
assert payload["period"] == "2026-06"
assert payload["liquidity"]["remaining_budget"] == "659.12"
assert payload["liquidity"]["cumulative_infrastructure_cost"] == "409.28"
assert payload["snapshot"]["monthly_infrastructure_cost"] == "29.73"
assert len(payload["history"]) == 18
assert payload["expense_record_count"] == 58
assert payload["cost_floor"]["active_price"] == "8.99"
assert len(payload["value_range"]["segments"]) == 2
assert payload["market_price"]["alternative_count"] == 4
assert payload["membership_analytics"]["active_members"] == 1
assert payload["usage"]["record_count"] == 1
assert len(payload["pricing_simulations"]["scenarios"]) == 3
assert len(payload["pricing_simulations"]["profile_comparisons"]) == 2
assert payload["pricing_simulations"]["primary_profile_id"] == "solo-builder"
assert payload["pricing_simulations"]["required_improvement_factor"] == "1.05"
assert payload["pricing_simulations"]["reference_model_id"] is not None
assert payload["customer_tuning"]["request_count"] == 2
assert payload["customer_tuning"]["accepted_request_ids"] == ["small-team-lower-usage-price"]
assert payload["provider_publication"]["provider"] == "stripe"
assert payload["provider_publication"]["model_id"] == "flat-899-eur-monthly"
assert payload["provider_publication"]["plan"]["summary"].startswith("stripe:")
assert payload["governance"]["policy"]["policy_id"] == "coulomb-governance-v1"
assert payload["governance"]["publication_assessment"]["decision"] == "approval_required"
assert payload["governance"]["safe_tuning_contracts"]
assert len(payload["boundary_validation"]["model_results"]) == 3
assert payload["boundary_validation"]["policy"]["target_margin_pct"] == "15"
assert any(
result["decision"] == "rejected"
for result in payload["boundary_validation"]["model_results"]
)
assert payload["recommendations"]
assert all("confidence" in item and "governance" in item for item in payload["recommendations"])
def test_payload_json_is_valid() -> None:
parsed = json.loads(payload_json(DATA_DIR, "2026-06"))
assert Decimal(parsed["payments"][0]["fees_amount"]) == Decimal("0.44")

View File

@@ -0,0 +1,168 @@
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from adaptive_pricing_core.boundary_engine import (
BoundaryPolicy,
CommitmentTerms,
PricingConfiguration,
validate_pricing_configuration,
)
from observatory.load import load_pricing_models
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _model(model_id: str):
return next(item for item in load_pricing_models(DATA_DIR) if item.id == model_id)
def test_commitment_backed_discount_is_accepted_when_economics_stay_strong() -> None:
result = validate_pricing_configuration(
PricingConfiguration(
model=_model("membership-plus-overage"),
segment="coulomb-social-members",
expected_usage_units=Decimal("100200"),
expected_usage_variance_pct=Decimal("25"),
allocated_fixed_cost=Decimal("2.00"),
unit_cost=Decimal("0.00000125"),
payment_fee_rate_pct=Decimal("5"),
usage_unit_price=Decimal("0.0015"),
commitment_terms=CommitmentTerms(
contract_duration_months=6,
minimum_monthly_turnover=Decimal("9.30"),
),
),
BoundaryPolicy(),
)
assert result.decision == "accepted"
assert result.valid is True
assert result.requires_approval is False
commitment_result = next(
item for item in result.constraints if item.id == "commitment-backed-concession"
)
assert commitment_result.status == "pass"
assert "minimum_monthly_turnover" in commitment_result.details["signals"]
def test_discount_without_commitment_is_rejected() -> None:
result = validate_pricing_configuration(
PricingConfiguration(
model=_model("membership-plus-overage"),
segment="coulomb-social-members",
expected_usage_units=Decimal("100200"),
expected_usage_variance_pct=Decimal("25"),
allocated_fixed_cost=Decimal("2.00"),
unit_cost=Decimal("0.00000125"),
payment_fee_rate_pct=Decimal("5"),
usage_unit_price=Decimal("0.0015"),
),
BoundaryPolicy(),
)
assert result.decision == "rejected"
assert result.valid is False
failing_ids = {
item.id for item in result.constraints if item.status == "fail" and item.severity == "hard"
}
assert "commitment-backed-concession" in failing_ids
def test_weak_commitment_trade_is_rejected() -> None:
result = validate_pricing_configuration(
PricingConfiguration(
model=_model("membership-plus-overage"),
segment="coulomb-social-members",
expected_usage_units=Decimal("100200"),
expected_usage_variance_pct=Decimal("25"),
allocated_fixed_cost=Decimal("2.00"),
unit_cost=Decimal("0.00000125"),
payment_fee_rate_pct=Decimal("5"),
usage_unit_price=Decimal("0.0015"),
commitment_terms=CommitmentTerms(
contract_duration_months=2,
minimum_monthly_turnover=Decimal("9.00"),
),
),
BoundaryPolicy(),
)
assert result.decision == "rejected"
commitment_result = next(
item for item in result.constraints if item.id == "commitment-backed-concession"
)
assert commitment_result.status == "fail"
def test_large_but_supported_concession_requires_approval() -> None:
result = validate_pricing_configuration(
PricingConfiguration(
model=_model("flat-899-eur-monthly"),
segment="coulomb-social-members",
allocated_fixed_cost=Decimal("1.00"),
payment_fee_rate_pct=Decimal("5"),
access_fee_amount=Decimal("7.50"),
commitment_terms=CommitmentTerms(
contract_duration_months=6,
minimum_monthly_turnover=Decimal("8.00"),
),
),
BoundaryPolicy(),
)
assert result.decision == "requires_approval"
assert result.valid is True
assert result.requires_approval is True
review_ids = {item.id for item in result.constraints if item.status == "review"}
assert "discount-approval-threshold" in review_ids
def test_zero_usage_flat_configuration_can_pass() -> None:
result = validate_pricing_configuration(
PricingConfiguration(
model=_model("flat-899-eur-monthly"),
segment="coulomb-social-members",
expected_usage_units=Decimal("0"),
expected_usage_variance_pct=Decimal("0"),
allocated_fixed_cost=Decimal("3.00"),
payment_fee_rate_pct=Decimal("5"),
),
BoundaryPolicy(),
)
assert result.decision == "accepted"
assert result.metrics.billable_usage_units == Decimal("0")
def test_high_fee_configuration_is_rejected() -> None:
result = validate_pricing_configuration(
PricingConfiguration(
model=_model("flat-899-eur-monthly"),
segment="coulomb-social-members",
allocated_fixed_cost=Decimal("1.00"),
payment_fee_rate_pct=Decimal("30"),
),
BoundaryPolicy(),
)
assert result.decision == "rejected"
payment_fee_result = next(item for item in result.constraints if item.id == "payment-fee-limit")
assert payment_fee_result.status == "fail"
def test_unprofitable_configuration_is_rejected() -> None:
result = validate_pricing_configuration(
PricingConfiguration(
model=_model("flat-899-eur-monthly"),
segment="coulomb-social-members",
allocated_fixed_cost=Decimal("10.00"),
payment_fee_rate_pct=Decimal("5"),
),
BoundaryPolicy(),
)
assert result.decision == "rejected"
assert result.metrics.monthly_margin < Decimal("0")
assert any(item.id == "cost-floor-coverage" and item.status == "fail" for item in result.constraints)

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
from pathlib import Path
from observatory.economics import build_snapshot
from observatory.load import (
load_ltv_scenarios,
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
)
from observatory.simulator import build_pricing_simulations
from observatory.usage import load_usage_records
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _snapshot(period: str = "2026-06"):
product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR)
members = load_membership(DATA_DIR)
payments = load_payment_records(DATA_DIR)
ledger = load_monthly_ledger(DATA_DIR)
return build_snapshot(period, product, models, members, payments, ledger)
def test_simulations_include_reference_model_and_profile_comparisons() -> None:
snapshot = _snapshot()
models = load_pricing_models(DATA_DIR)
simulations = build_pricing_simulations(
snapshot,
models,
snapshot.cost_per_member,
usage_records=load_usage_records(DATA_DIR),
scenario_catalog=load_ltv_scenarios(DATA_DIR),
)
assert simulations["primary_profile_id"] == "solo-builder"
assert simulations["reference_model_id"] is not None
assert simulations["best_ltv_scenario_id"] is not None
assert len(simulations["profile_comparisons"]) == 2
assert simulations["scenarios"][0]["average_comparable_customer_lifetime_value"] is not None
assert simulations["scenarios"][0]["sensitivity"]
def test_small_team_profile_has_reference_and_non_passing_candidates() -> None:
snapshot = _snapshot()
models = load_pricing_models(DATA_DIR)
simulations = build_pricing_simulations(
snapshot,
models,
snapshot.cost_per_member,
usage_records=load_usage_records(DATA_DIR),
scenario_catalog=load_ltv_scenarios(DATA_DIR),
)
small_team = next(
item for item in simulations["profile_comparisons"] if item["profile"]["id"] == "small-team"
)
assert small_team["reference_model_id"] is not None
assert small_team["best_valid_model_id"] is not None
assert any(
not comparison["passes_required_improvement"]
for comparison in small_team["comparisons"]
if comparison["model_id"] != small_team["reference_model_id"]
)

View File

@@ -0,0 +1,149 @@
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from adaptive_pricing_core.customer_tuning import CustomerTuningRequest, solve_customer_tuning
from observatory.boundary import build_boundary_policy
from observatory.economics import build_snapshot
from observatory.load import (
load_ltv_scenarios,
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
)
from observatory.ltv import _configuration, _ltv_policy, _profile, _usage_unit_cost
from observatory.tuning import build_customer_tuning_pilot
from observatory.usage import load_usage_records
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _scenario_inputs(profile_id: str = "small-team"):
product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR)
members = load_membership(DATA_DIR)
payments = load_payment_records(DATA_DIR)
ledger = load_monthly_ledger(DATA_DIR)
snapshot = build_snapshot("2026-06", product, models, members, payments, ledger)
usage_records = load_usage_records(DATA_DIR)
scenario_catalog = load_ltv_scenarios(DATA_DIR)
profile = _profile(next(item for item in scenario_catalog["profiles"] if item["id"] == profile_id))
observed_usage_unit_cost = _usage_unit_cost(usage_records, snapshot.period)
return snapshot, models, usage_records, scenario_catalog, profile, observed_usage_unit_cost
def test_lower_usage_price_request_can_stay_seller_safe() -> None:
(
snapshot,
models,
usage_records,
scenario_catalog,
profile,
observed_usage_unit_cost,
) = _scenario_inputs()
model = next(item for item in models if item.id == "membership-plus-overage")
outcome = solve_customer_tuning(
_configuration(model, profile, snapshot, observed_usage_unit_cost),
[
_configuration(candidate, profile, snapshot, observed_usage_unit_cost)
for candidate in models
if candidate.status in ("active", "candidate")
],
profile,
build_boundary_policy(snapshot),
_ltv_policy(scenario_catalog),
CustomerTuningRequest(
included_units=Decimal("50000"),
contract_duration_months=3,
preference="lower_usage_price",
approval_mode="self_serve_only",
),
)
assert outcome.decision == "accepted"
assert outcome.reference_model_id == "flat-899-eur-monthly"
assert outcome.passes_required_improvement is True
assert outcome.solved_usage_unit_price < Decimal("0.002")
assert "lower_included_usage" in outcome.tradeoffs
assert "longer_contract_duration" in outcome.tradeoffs
def test_high_included_request_is_rejected_for_self_serve() -> None:
(
snapshot,
models,
_usage_records,
scenario_catalog,
profile,
observed_usage_unit_cost,
) = _scenario_inputs()
model = next(item for item in models if item.id == "membership-plus-overage")
outcome = solve_customer_tuning(
_configuration(model, profile, snapshot, observed_usage_unit_cost),
[
_configuration(candidate, profile, snapshot, observed_usage_unit_cost)
for candidate in models
if candidate.status in ("active", "candidate")
],
profile,
build_boundary_policy(snapshot),
_ltv_policy(scenario_catalog),
CustomerTuningRequest(
included_units=Decimal("150000"),
contract_duration_months=3,
preference="lower_usage_price",
approval_mode="self_serve_only",
),
)
assert outcome.decision == "rejected"
assert outcome.passes_required_improvement is True
assert any(
constraint.id == "discount-exposure-limit"
for constraint in outcome.binding_constraints
)
def test_customer_tuning_pilot_surfaces_accepted_and_rejected_requests() -> None:
snapshot, models, usage_records, scenario_catalog, _profile_data, _usage_unit_cost_value = _scenario_inputs()
pilot = build_customer_tuning_pilot(
snapshot,
models,
usage_records,
scenario_catalog,
{
"requests": [
{
"id": "accepted",
"name": "Accepted",
"profile_id": "small-team",
"model_id": "membership-plus-overage",
"preference": "lower_usage_price",
"approval_mode": "self_serve_only",
"selected_tunables": {
"included_tokens": "50000",
"contract_duration_months": 3,
},
},
{
"id": "rejected",
"name": "Rejected",
"profile_id": "small-team",
"model_id": "membership-plus-overage",
"preference": "lower_usage_price",
"approval_mode": "self_serve_only",
"selected_tunables": {
"included_tokens": "150000",
"contract_duration_months": 3,
},
},
]
},
)
assert pilot["request_count"] == 2
assert pilot["accepted_request_ids"] == ["accepted"]
assert {item["result"]["decision"] for item in pilot["requests"]} == {"accepted", "rejected"}

View File

@@ -0,0 +1,117 @@
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from observatory.economics import active_members, build_liquidity_summary, build_snapshot
from observatory.ledger import aggregate_infrastructure_by_period, build_monthly_ledger
from observatory.load import (
load_budget,
load_expense_records,
load_fx_rates,
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
)
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def test_active_members_counts_only_active_status() -> None:
members = load_membership(DATA_DIR)
assert active_members(members) == 1
def test_infrastructure_aggregated_from_domain_expense_records() -> None:
expenses = load_expense_records(DATA_DIR)
fx = load_fx_rates(DATA_DIR)
totals = aggregate_infrastructure_by_period(expenses, fx)
assert totals["2025-01"] == Decimal("20.74")
assert totals["2026-02"] == Decimal("20.74")
assert totals["2026-03"] == Decimal("29.73")
assert totals["2026-06"] == Decimal("29.73")
def test_monthly_ledger_starts_january_2025() -> None:
ledger = load_monthly_ledger(DATA_DIR)
assert ledger[0].period == "2025-01"
assert len(ledger) == 18
march = next(row for row in ledger if row.period == "2025-03")
june = next(row for row in ledger if row.period == "2026-06")
assert march.infrastructure_cost == Decimal("20.74")
assert march.payment_processing_cost == Decimal("0.00")
assert june.infrastructure_cost == Decimal("29.73")
assert june.payment_processing_cost == Decimal("0.44")
assert june.gross_revenue == Decimal("8.99")
def test_build_snapshot_june_2026_domain_only_infrastructure() -> None:
product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR)
members = load_membership(DATA_DIR)
payments = load_payment_records(DATA_DIR)
ledger = load_monthly_ledger(DATA_DIR)
snapshot = build_snapshot("2026-06", product, models, members, payments, ledger)
assert snapshot.monthly_infrastructure_cost == Decimal("29.73")
assert snapshot.monthly_payment_processing_cost == Decimal("0.44")
assert snapshot.monthly_total_platform_cost == Decimal("30.17")
assert snapshot.period_net_liquidity == Decimal("-21.18")
assert snapshot.liquidity_status == "burning"
def test_liquidity_summary_with_actual_domain_costs() -> None:
budget = load_budget(DATA_DIR)
payments = load_payment_records(DATA_DIR)
ledger = load_monthly_ledger(DATA_DIR)
summary = build_liquidity_summary(budget, payments, ledger, "2026-06")
assert summary.cumulative_infrastructure_cost == Decimal("409.28")
assert summary.cumulative_payment_processing_cost == Decimal("3.52")
assert summary.cumulative_total_platform_cost == Decimal("412.80")
assert summary.cumulative_member_payments == Decimal("68.40")
assert summary.cumulative_net_liquidity == Decimal("-340.88")
assert summary.remaining_budget == Decimal("659.12")
assert summary.liquidity_status == "burning"
assert summary.months_tracked == 18
def test_build_monthly_ledger_matches_loader() -> None:
budget = load_budget(DATA_DIR)
expenses = load_expense_records(DATA_DIR)
payments = load_payment_records(DATA_DIR)
members = load_membership(DATA_DIR)
fx = load_fx_rates(DATA_DIR)
assert build_monthly_ledger(budget, expenses, payments, members, fx) == load_monthly_ledger(
DATA_DIR
)
def test_payment_records_match_stripe_actuals_for_tegwick() -> None:
payments = load_payment_records(DATA_DIR)
nov = next(p for p in payments if p.period == "2025-11")
assert nov.gross_amount == Decimal("8.99")
assert nov.fees_amount == Decimal("0.44")
assert nov.net_amount == Decimal("8.55")
assert nov.member_username == "tegwick"
assert nov.payout_account == "binky-hedgehog"
assert nov.source == "stripe"
def test_dashboard_notes_expense_record_source() -> None:
from observatory.dashboard import generate_dashboard
report = generate_dashboard(DATA_DIR, "2026-06")
assert "expense and payment record ledgers" in report
assert "409.28" in report
assert "659.12" in report
assert "coulomb.social" not in report # dashboard shows aggregates, not domain names

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
from pathlib import Path
from observatory.api import build_dashboard_payload
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def test_governance_payload_contains_policy_health_and_audit_surfaces() -> None:
payload = build_dashboard_payload(DATA_DIR, "2026-06")
governance = payload["governance"]
assert governance["policy"]["policy_id"] == "coulomb-governance-v1"
assert governance["publication_assessment"]["decision"] == "approval_required"
assert governance["audit_surface"]["provider"] == "stripe"
assert governance["audit_surface"]["revision_count"] == 0
assert any(
check["id"] == "provider-execution-health" and check["status"] == "warn"
for check in governance["health_checks"]
)
def test_safe_tuning_contract_stays_pilot_only_and_hides_customer_visibility() -> None:
payload = build_dashboard_payload(DATA_DIR, "2026-06")
contract = next(
item
for item in payload["governance"]["safe_tuning_contracts"]
if item["model_id"] == "membership-plus-overage"
)
assert contract["mode"] == "pilot_only"
assert contract["customer_visible"] is False
assert any(example["outcome"] == "accepted" for example in contract["examples"])
assert all(example["visible_to_customer"] is False for example in contract["examples"])
def test_recommendations_include_governed_execution_gate() -> None:
payload = build_dashboard_payload(DATA_DIR, "2026-06")
execution_gate = next(
item for item in payload["recommendations"] if item["id"] == "execution-governance-gate"
)
assert execution_gate["recommendation_type"] == "execution"
assert execution_gate["governance"]["decision"] == "approval_required"
assert execution_gate["confidence"] == "0.88"
assert execution_gate["risks"]
assert execution_gate["supporting_observations"]

View File

@@ -0,0 +1,42 @@
from __future__ import annotations
from pathlib import Path
from observatory.importers.bubble import import_membership
from observatory.importers.openrouter import import_usage
from observatory.importers.stripe import import_payments
IMPORTS = Path(__file__).resolve().parent.parent / "data" / "imports"
def test_bubble_import_maps_active_member() -> None:
import json
export = json.loads((IMPORTS / "bubble-export.sample.json").read_text(encoding="utf-8"))
payload = import_membership(export)
assert len(payload["members"]) == 1
assert payload["members"][0]["id"] == "member-tegwick"
assert payload["members"][0]["status"] == "active"
assert payload["members"][0]["source"] == "bubble"
def test_stripe_import_normalises_charge() -> None:
import json
export = json.loads((IMPORTS / "stripe-export.sample.json").read_text(encoding="utf-8"))
payload = import_payments(export)
assert payload["records"][0]["gross_amount"] == "8.99"
assert payload["records"][0]["net_amount"] == "8.55"
assert payload["records"][0]["member_username"] == "tegwick"
def test_openrouter_import_converts_cost_to_eur() -> None:
import json
export = json.loads((IMPORTS / "openrouter-export.sample.json").read_text(encoding="utf-8"))
payload = import_usage(export, fx_usd_eur="0.92")
assert payload["records"][0]["cost_eur"] == "0.06"
assert payload["records"][0]["member_id"] == "member-tegwick"

View File

@@ -0,0 +1,105 @@
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from observatory.allocation import build_cost_allocation
from observatory.api import build_dashboard_payload
from observatory.credits import build_credit_summary, load_credit_wallets
from observatory.economics import build_snapshot
from observatory.load import (
load_ltv_scenarios,
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
)
from observatory.membership_analytics import build_membership_analytics
from observatory.recommendations import build_pricing_recommendations
from observatory.simulator import build_pricing_simulations
from observatory.usage import build_usage_summary, load_usage_records
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _snapshot(period: str = "2026-06"):
product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR)
members = load_membership(DATA_DIR)
payments = load_payment_records(DATA_DIR)
ledger = load_monthly_ledger(DATA_DIR)
return build_snapshot(period, product, models, members, payments, ledger)
def test_membership_analytics_counts_active_member() -> None:
members = load_membership(DATA_DIR)
analytics = build_membership_analytics(members, "2026-06", ["2026-05", "2026-06"])
assert analytics["active_members"] == 1
assert analytics["total_members"] == 1
def test_usage_summary_attributes_member_cost() -> None:
records = load_usage_records(DATA_DIR)
summary = build_usage_summary(records, "2026-06")
assert summary["record_count"] == 1
assert summary["by_member"]["member-tegwick"] == Decimal("0.06")
def test_cost_allocation_includes_ai_variable_cost() -> None:
snapshot = _snapshot()
allocation = build_cost_allocation(snapshot, load_usage_records(DATA_DIR))
assert allocation["variable_ai_eur"] == Decimal("0.06")
assert allocation["cost_floor_eur"] == snapshot.cost_per_member
def test_pricing_simulator_compares_candidate_models() -> None:
snapshot = _snapshot()
models = load_pricing_models(DATA_DIR)
simulations = build_pricing_simulations(
snapshot,
models,
Decimal("0.06"),
usage_records=load_usage_records(DATA_DIR),
scenario_catalog=load_ltv_scenarios(DATA_DIR),
)
assert len(simulations["scenarios"]) == 3
assert simulations["active_scenario_id"] == "flat-899-eur-monthly"
assert simulations["best_ltv_scenario_id"] is not None
assert simulations["reference_model_id"] is not None
def test_credit_summary_tracks_remaining_allowance() -> None:
wallets = load_credit_wallets(DATA_DIR)
summary = build_credit_summary(wallets, {"member-tegwick": Decimal("0.06")}, "2026-06")
assert summary["wallets"][0]["remaining_eur"] == Decimal("1.94")
def test_recommendations_include_hold_or_action() -> None:
payload = build_dashboard_payload(DATA_DIR, "2026-06")
recs = build_pricing_recommendations(
payload["cost_floor"],
payload["value_range"],
payload["market_price"],
payload["pricing_simulations"],
payload["usage"],
)
assert recs
assert recs[0]["id"] in {"margin-pressure", "usage-pricing-signal", "value-headroom", "hold-course"}
def test_dashboard_payload_includes_mvp_sections() -> None:
payload = build_dashboard_payload(DATA_DIR, "2026-06")
assert "membership_analytics" in payload
assert "usage" in payload
assert "cost_allocation" in payload
assert "pricing_simulations" in payload
assert "credit_wallets" in payload
assert "recommendations" in payload

View File

@@ -0,0 +1,61 @@
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from observatory.economics import build_snapshot
from observatory.load import (
load_market_signals,
load_membership,
load_monthly_ledger,
load_payment_records,
load_pricing_models,
load_product,
load_value_range,
)
from observatory.pricing_context import (
build_cost_floor,
build_market_price_view,
build_value_range_view,
)
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _snapshot_for(period: str = "2026-06"):
product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR)
members = load_membership(DATA_DIR)
payments = load_payment_records(DATA_DIR)
ledger = load_monthly_ledger(DATA_DIR)
return build_snapshot(period, product, models, members, payments, ledger)
def test_cost_floor_derives_from_snapshot() -> None:
snapshot = _snapshot_for()
models = load_pricing_models(DATA_DIR)
floor = build_cost_floor(snapshot, models)
assert floor["cost_per_member"] == snapshot.cost_per_member
assert floor["active_price"] == Decimal("8.99")
assert floor["active_model_id"] == "flat-899-eur-monthly"
def test_value_range_computes_headroom() -> None:
snapshot = _snapshot_for()
product = load_product(DATA_DIR)
models = load_pricing_models(DATA_DIR)
view = build_value_range_view(load_value_range(DATA_DIR), snapshot, product, models)
assert view["current_price_eur"] == Decimal("8.99")
assert len(view["segments"]) == 2
solo = next(item for item in view["segments"] if item["id"] == "solo-builder")
assert solo["headroom_to_high_eur"] == Decimal("10.01")
def test_market_price_summarises_alternatives() -> None:
view = build_market_price_view(load_market_signals(DATA_DIR))
assert view["alternative_count"] == 4
assert view["market_low_eur"] == Decimal("4.00")
assert view["market_high_eur"] == Decimal("49.00")

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from adaptive_pricing_core.pricing_models import validate_pricing_catalog
from observatory.load import load_pricing_models
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def test_coulomb_pricing_catalog_validates() -> None:
models = load_pricing_models(DATA_DIR)
assert validate_pricing_catalog(models) == {}
def test_hybrid_model_preserves_usage_component_and_tuning_metadata() -> None:
models = load_pricing_models(DATA_DIR)
model = next(item for item in models if item.id == "membership-plus-overage")
usage_component = next(component for component in model.charge_components if component.kind == "usage")
assert usage_component.meter == "openrouter_tokens"
assert usage_component.included_units == Decimal("100000")
assert usage_component.unit_price == Decimal("0.002")
assert any(parameter.parameter_class == "customer_tunable" for parameter in model.tunable_parameters)
def test_flat_model_still_exposes_access_fee_compatibility_fields() -> None:
models = load_pricing_models(DATA_DIR)
model = next(item for item in models if item.id == "flat-899-eur-monthly")
assert model.access_fee_amount == Decimal("8.99")
assert model.access_fee_cadence == "monthly"
assert len(model.charge_components) == 1

View File

@@ -0,0 +1,124 @@
from __future__ import annotations
import json
from pathlib import Path
from observatory.load import load_pricing_models, load_product
from observatory.publication import (
build_stripe_publication_preview,
publish_to_stripe_shadow,
rollback_stripe_shadow,
)
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _catalog():
return load_product(DATA_DIR), load_pricing_models(DATA_DIR)
def test_overage_preview_includes_meter_and_approximate_mapping(tmp_path: Path) -> None:
product, models = _catalog()
preview = build_stripe_publication_preview(
product,
models,
DATA_DIR,
model_id="membership-plus-overage",
state_path=tmp_path / "stripe-state.json",
)
assert preview["artifact_counts"]["exact"] >= 3
assert preview["artifact_counts"]["approximate"] >= 1
assert preview["artifact_counts"]["unsupported"] == 0
assert any(
operation["provider_object_type"] == "billing_meter"
for operation in preview["plan"]["operations"]
)
def test_credit_allowance_preview_marks_unsupported_usage_mapping(tmp_path: Path) -> None:
product, models = _catalog()
preview = build_stripe_publication_preview(
product,
models,
DATA_DIR,
model_id="membership-plus-credits",
state_path=tmp_path / "stripe-state.json",
)
unsupported = {
artifact["source_key"]
for artifact in preview["plan"]["unsupported_artifacts"]
}
assert "price:membership-plus-credits:ai-credit-allowance" in unsupported
assert preview["artifact_counts"]["unsupported"] >= 1
def test_publication_is_idempotent_and_detects_drift(tmp_path: Path) -> None:
product, models = _catalog()
state_path = tmp_path / "stripe-state.json"
publish_to_stripe_shadow(
product,
models,
DATA_DIR,
model_id="flat-899-eur-monthly",
state_path=state_path,
)
preview = build_stripe_publication_preview(
product,
models,
DATA_DIR,
model_id="flat-899-eur-monthly",
state_path=state_path,
)
assert {operation["kind"] for operation in preview["plan"]["operations"]} == {"noop"}
raw_state = json.loads(state_path.read_text(encoding="utf-8"))
price_artifact = next(
artifact
for artifact in raw_state["artifacts"]
if artifact["provider_id"] == "price--flat-899-eur-monthly--membership-access"
)
price_artifact["payload"]["unit_amount_decimal"] = "9.49"
state_path.write_text(json.dumps(raw_state, indent=2), encoding="utf-8")
drifted = build_stripe_publication_preview(
product,
models,
DATA_DIR,
model_id="flat-899-eur-monthly",
state_path=state_path,
)
assert drifted["plan"]["drift"]
assert any(operation["kind"] == "update" for operation in drifted["plan"]["operations"])
def test_publication_rollback_restores_prior_revision(tmp_path: Path) -> None:
product, models = _catalog()
state_path = tmp_path / "stripe-state.json"
first = publish_to_stripe_shadow(
product,
models,
DATA_DIR,
model_id="flat-899-eur-monthly",
state_path=state_path,
)
publish_to_stripe_shadow(
product,
models,
DATA_DIR,
model_id="membership-plus-overage",
state_path=state_path,
)
rollback = rollback_stripe_shadow(
DATA_DIR,
first["result"]["revision"]["revision_id"],
state_path=state_path,
)
assert rollback["result"]["state"]["active_model_id"] == "flat-899-eur-monthly"
assert rollback["result"]["revision"]["summary"].startswith("Rolled back")

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
import importlib
import threading
from http.server import HTTPServer
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
VENDOR = ROOT / "ui" / "vendor" / "whynot-design"
REQUIRED_VENDOR_FILES = [
VENDOR / ".whynot-design-ref",
VENDOR / "colors_and_type.css",
VENDOR / "components.css",
VENDOR / "index.js",
VENDOR / "elements" / "atoms.js",
VENDOR / "elements" / "chrome.js",
VENDOR / "elements" / "form.js",
VENDOR / "elements" / "layout.js",
VENDOR / "elements" / "icons.js",
VENDOR / "elements" / "_styles.js",
VENDOR / "tokens" / "colors.json",
]
def test_vendor_tree_is_complete() -> None:
missing = [path for path in REQUIRED_VENDOR_FILES if not path.exists()]
assert not missing, f"Missing vendored whynot-design files: {missing}"
def test_vendor_ref_is_pinned() -> None:
ref = (VENDOR / ".whynot-design-ref").read_text(encoding="utf-8").strip()
assert len(ref) == 40
def test_server_serves_vendor_modules() -> None:
server_module = importlib.import_module("observatory.server")
handler = server_module.ObservatoryHandler
handler.data_dir = ROOT / "data"
try:
httpd = HTTPServer(("127.0.0.1", 0), handler)
except PermissionError:
pytest.skip("local socket binds are not permitted in this execution environment")
port = httpd.server_address[1]
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
try:
import urllib.request
index = urllib.request.urlopen(f"http://127.0.0.1:{port}/", timeout=2)
assert "wn-top-nav" in index.read().decode("utf-8")
module = urllib.request.urlopen(
f"http://127.0.0.1:{port}/ui/vendor/whynot-design/index.js",
timeout=2,
)
assert module.headers["Content-Type"].startswith("application/javascript")
assert "defineAtoms" in module.read().decode("utf-8")
finally:
httpd.shutdown()
thread.join(timeout=2)

View File

@@ -0,0 +1,416 @@
const euro = (value) => `${Number(value).toFixed(2)}`;
const SECTION_META = {
"cost-floor": {
eyebrow: "Economic Observatory · Cost Floor",
title: "Operator liquidity",
lede: (data) =>
`Period ${data.period}. Minimum viable economics: platform cost, margin, and liquidity burn from ledgers.`,
},
"value-range": {
eyebrow: "Economic Observatory · Value Range",
title: "Customer value bands",
lede: (data) =>
`Period ${data.period}. Segment-level willingness-to-pay hypotheses above the cost floor.`,
},
"market-price": {
eyebrow: "Economic Observatory · Market Price",
title: "Competitive context",
lede: (data) =>
`Reviewed ${data.market_price.last_reviewed ?? "—"}. Evidence about alternatives, capabilities, and public pricing.`,
},
};
let currentData = null;
let activeSection = "cost-floor";
function normalizeData(data) {
const snapshot = data.snapshot ?? {};
const active = (data.pricing_models ?? []).find((model) => model.status === "active");
if (!data.cost_floor) {
data.cost_floor = {
cost_per_member: snapshot.cost_per_member ?? "0",
monthly_total_platform_cost: snapshot.monthly_total_platform_cost ?? "0",
active_price: active?.access_fee_amount ?? snapshot.monthly_revenue ?? "0",
active_model_name: active?.name ?? null,
};
}
if (!data.value_range) {
data.value_range = {
current_price_eur: active?.access_fee_amount ?? snapshot.monthly_revenue ?? "0",
aggregate_low_eur: active?.access_fee_amount ?? "0",
aggregate_high_eur: active?.access_fee_amount ?? "0",
cost_per_member_eur: snapshot.cost_per_member ?? "0",
segments: [],
value_drivers: [],
notes: "Value range data unavailable — restart observatory.server to load the latest API.",
};
}
if (!data.market_price) {
data.market_price = {
alternative_count: 0,
priced_alternative_count: 0,
market_low_eur: null,
market_high_eur: null,
last_reviewed: null,
alternatives: [],
notes: "Market signals unavailable — restart observatory.server to load the latest API.",
};
}
return data;
}
async function loadDashboard(period) {
const query = period ? `?period=${encodeURIComponent(period)}` : "";
const response = await fetch(`/api/dashboard${query}`);
if (!response.ok) throw new Error("Failed to load dashboard");
return response.json();
}
function setBadge(el, status) {
el.textContent = status;
el.active = status === "generating" || status === "neutral";
el.draft = status === "burning";
}
function setSection(sectionId) {
activeSection = sectionId;
document.querySelectorAll(".obs-panel").forEach((panel) => {
const active = panel.dataset.section === sectionId;
panel.hidden = !active;
panel.classList.toggle("obs-panel--active", active);
});
document.querySelectorAll("wn-sidebar-item[data-section]").forEach((item) => {
item.active = item.dataset.section === sectionId;
});
const meta = SECTION_META[sectionId];
const header = document.getElementById("page-header");
header.eyebrow = meta.eyebrow;
header.title = meta.title;
if (currentData) {
header.lede = meta.lede(currentData);
}
const badge = document.getElementById("liquidity-badge");
badge.style.display = sectionId === "cost-floor" ? "" : "none";
}
function bindNavigation() {
document.querySelectorAll("wn-sidebar-item[data-section]").forEach((item) => {
item.addEventListener("click", () => setSection(item.dataset.section));
});
}
function renderCostFloor(data) {
const { snapshot, liquidity } = data;
const cards = [
{
label: "Remaining budget",
value: euro(liquidity.remaining_budget),
sub: `${liquidity.months_tracked} months tracked`,
},
{
label: "Period net liquidity",
value: euro(snapshot.period_net_liquidity),
sub: snapshot.liquidity_status,
},
{
label: "Cost per member",
value: euro(data.cost_floor.cost_per_member),
sub: `Platform ${euro(data.cost_floor.monthly_total_platform_cost)} / mo`,
},
{
label: "Gross margin",
value: euro(snapshot.gross_margin),
sub: `${snapshot.gross_margin_pct}% of gross revenue`,
},
{
label: "Infrastructure / month",
value: euro(snapshot.monthly_infrastructure_cost),
sub: `Processing ${euro(snapshot.monthly_payment_processing_cost)}`,
},
{
label: "Active price",
value: euro(data.cost_floor.active_price),
sub: data.cost_floor.active_model_name ?? "—",
},
];
document.getElementById("metric-grid").innerHTML = cards
.map(
(card) => `
<wn-card size="sm">
<wn-eyebrow slot="header">${card.label}</wn-eyebrow>
<div class="obs-metric__value">${card.value}</div>
<span slot="footer">${card.sub}</span>
</wn-card>`
)
.join("");
const initial = Number(liquidity.initial_budget);
const remaining = Number(liquidity.remaining_budget);
const pct = Math.max(0, Math.min(100, (remaining / initial) * 100));
const banner = document.getElementById("budget-banner");
document.getElementById("budget-fill").style.width = `${pct}%`;
document.getElementById("budget-caption").textContent =
remaining >= 0 ? "Within allocated budget" : "Over allocated budget";
banner.variant = remaining < initial * 0.25 ? "warn" : "info";
document.getElementById("budget-stats").innerHTML = [
["Initial budget", euro(initial)],
["Cumulative member payments (net)", euro(liquidity.cumulative_member_payments)],
["Cumulative infrastructure", euro(liquidity.cumulative_infrastructure_cost)],
["Cumulative processing", euro(liquidity.cumulative_payment_processing_cost)],
]
.map(
([label, value]) => `
<wn-field-row label="${label}" narrow>
<span class="wn-field-row__value">${value}</span>
</wn-field-row>`
)
.join("");
const max = Math.max(...data.history.map((row) => Math.abs(Number(row.net_liquidity))), 1);
document.getElementById("liquidity-chart").innerHTML = data.history
.map((row) => {
const value = Number(row.net_liquidity);
const width = (Math.abs(value) / max) * 50;
const barClass = value < 0 ? "obs-liquidity-row__bar--neg" : "obs-liquidity-row__bar--pos";
const valueClass = value < 0 ? "obs-liquidity-row__value--neg" : "";
return `
<div class="obs-liquidity-row">
<div class="obs-liquidity-row__period">${row.period}</div>
<div class="obs-liquidity-row__bar-wrap">
<div class="obs-liquidity-row__bar ${barClass}" style="width:${width}%"></div>
</div>
<div class="obs-liquidity-row__value ${valueClass}">${euro(value)}</div>
</div>`;
})
.join("");
const domains = data.infrastructure.domains?.domains ?? [];
const servers = data.infrastructure.virtual_servers?.servers ?? [];
const stripe = data.infrastructure.stripe?.membership ?? {};
const payout = data.infrastructure.stripe?.payout_account ?? "payout";
const infraItems = [
...domains.map(
(d) =>
`<wn-field-row label="${d.name}" narrow><span class="wn-field-row__value">${d.monthly_eur} EUR/mo · ${d.tld}</span></wn-field-row>`
),
...servers.map(
(s) =>
`<wn-field-row label="${s.name}" narrow><span class="wn-field-row__value">${s.monthly_eur} EUR/mo · since ${s.started}</span></wn-field-row>`
),
`<wn-field-row label="Stripe · ${stripe.member_username ?? "member"}" narrow><span class="wn-field-row__value">${stripe.gross_monthly_eur} gross · ${stripe.fee_monthly_eur} fee · ${stripe.net_payout_monthly_eur} net to ${payout}</span></wn-field-row>`,
];
document.getElementById("infra-stack").innerHTML = `<div class="obs-infra-list">${infraItems.join("")}</div>`;
document.getElementById("history-body").innerHTML = data.history
.map(
(row) => `
<tr>
<td class="mono">${row.period}</td>
<td class="mono">${row.active_members}</td>
<td class="obs-num mono">${euro(row.gross_revenue)}</td>
<td class="obs-num mono">${euro(row.infrastructure_cost)}</td>
<td class="obs-num mono">${euro(row.payment_processing_cost)}</td>
<td class="obs-num mono">${euro(row.net_liquidity)}</td>
</tr>`
)
.join("");
document.getElementById("pricing-body").innerHTML = data.pricing_models
.map(
(model) => `
<tr>
<td class="mono">${model.id}</td>
<td>${model.name}</td>
<td class="mono">${model.model_type}</td>
<td><wn-tag>${model.status}</wn-tag></td>
</tr>`
)
.join("");
setBadge(document.getElementById("liquidity-badge"), snapshot.liquidity_status);
}
function renderValueRangeBand(low, high, current) {
const min = Number(low);
const max = Number(high);
const cur = Number(current);
const span = Math.max(max - min, 0.01);
const curPct = Math.max(0, Math.min(100, ((cur - min) / span) * 100));
return `
<div class="obs-range">
<div class="obs-range__labels">
<span class="mono">${euro(min)}</span>
<span class="mono obs-range__current">${euro(cur)} now</span>
<span class="mono">${euro(max)}</span>
</div>
<div class="obs-range__track">
<div class="obs-range__fill" style="width:${curPct}%"></div>
<div class="obs-range__marker" style="left:${curPct}%"></div>
</div>
</div>`;
}
function renderValueRange(data) {
const vr = data.value_range;
document.getElementById("value-summary").innerHTML = [
{
label: "Current price",
value: euro(vr.current_price_eur),
sub: data.cost_floor.active_model_name ?? "—",
},
{
label: "Aggregate band",
value: `${euro(vr.aggregate_low_eur)} ${euro(vr.aggregate_high_eur)}`,
sub: `${vr.segments.length} segments`,
},
{
label: "Cost per member",
value: euro(vr.cost_per_member_eur),
sub: "Cost floor reference",
},
]
.map(
(card) => `
<wn-card size="sm">
<wn-eyebrow slot="header">${card.label}</wn-eyebrow>
<div class="obs-metric__value obs-metric__value--sm">${card.value}</div>
<span slot="footer">${card.sub}</span>
</wn-card>`
)
.join("");
document.getElementById("value-range-notes").textContent = vr.notes || "";
document.getElementById("value-segments").innerHTML = vr.segments
.map(
(segment) => `
<wn-card>
<wn-eyebrow slot="header">${segment.name}</wn-eyebrow>
<wn-tag slot="header">${segment.confidence}</wn-tag>
${renderValueRangeBand(segment.low_eur, segment.high_eur, vr.current_price_eur)}
<p class="small">${segment.evidence}</p>
<span slot="footer">Headroom ${euro(segment.headroom_to_high_eur)}</span>
<span slot="footer">${segment.drivers.join(" · ")}</span>
</wn-card>`
)
.join("");
document.getElementById("value-drivers").innerHTML = vr.value_drivers
.map(
(driver) => `
<wn-field-row label="${driver.label}" narrow>
<span class="wn-field-row__value">${driver.note}</span>
<span class="wn-field-row__aside">${driver.strength}</span>
</wn-field-row>`
)
.join("");
}
function renderMarketPrice(data) {
const market = data.market_price;
const span =
market.market_low_eur != null && market.market_high_eur != null
? `${euro(market.market_low_eur)} ${euro(market.market_high_eur)}`
: "—";
document.getElementById("market-summary").innerHTML = [
{
label: "Alternatives tracked",
value: market.alternative_count,
sub: `${market.priced_alternative_count} with public monthly price`,
},
{
label: "Priced span",
value: span,
sub: `Reviewed ${market.last_reviewed ?? "—"}`,
},
{
label: "Coulomb list price",
value: euro(data.value_range.current_price_eur),
sub: data.cost_floor.active_model_name ?? "—",
},
]
.map(
(card) => `
<wn-card size="sm">
<wn-eyebrow slot="header">${card.label}</wn-eyebrow>
<div class="obs-metric__value obs-metric__value--sm">${card.value}</div>
<span slot="footer">${card.sub}</span>
</wn-card>`
)
.join("");
document.getElementById("market-notes").textContent = market.notes || "";
document.getElementById("market-body").innerHTML = market.alternatives
.map((alt) => {
const price =
alt.price_monthly_eur && Number(alt.price_monthly_eur) > 0
? euro(alt.price_monthly_eur)
: "—";
const features = (alt.features ?? []).join(", ");
return `
<tr>
<td><strong>${alt.name}</strong><div class="small mono">${alt.id}</div></td>
<td><wn-tag>${alt.category}</wn-tag></td>
<td class="obs-num mono">${price}</td>
<td><wn-stage-dot level="${alt.signal_level}">${alt.signal_level}</wn-stage-dot></td>
<td>${features}</td>
<td class="small">${alt.source} · ${alt.observed}<br>${alt.note}</td>
</tr>`;
})
.join("");
}
function populatePeriods(history, current) {
const select = document.getElementById("period-select");
select.innerHTML = [...history].reverse().map(
(row) => `<option value="${row.period}" ${row.period === current ? "selected" : ""}>${row.period}</option>`
);
select.value = current;
if (!select._obsBound) {
select.addEventListener("wn-change", async (event) => {
const next = await loadDashboard(event.detail.value);
render(next);
});
select._obsBound = true;
}
}
async function loadDesignRefLabel() {
try {
const response = await fetch("/ui/vendor/whynot-design/.whynot-design-ref");
if (!response.ok) return;
const ref = (await response.text()).trim().slice(0, 7);
const label = document.getElementById("design-ref-label");
if (label && ref) label.textContent = `whynot-design @ ${ref}`;
} catch {
// optional footer detail
}
}
function render(data) {
currentData = normalizeData(data);
document.getElementById("design-link").href = data.design_reference;
loadDesignRefLabel();
setSection(activeSection);
renderCostFloor(data);
renderValueRange(data);
renderMarketPrice(data);
populatePeriods(data.history, data.period);
}
bindNavigation();
loadDashboard()
.then(render)
.catch((error) => {
document.body.innerHTML = `<main class="wn-main"><wn-banner variant="error" title="Load failed">${error}</wn-banner></main>`;
});

View File

@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coulomb · Economic Observatory</title>
<link rel="stylesheet" href="/ui/vendor/whynot-design/colors_and_type.css" />
<link rel="stylesheet" href="/ui/vendor/whynot-design/components.css" />
<link rel="stylesheet" href="/ui/styles.css" />
<script type="importmap">
{
"imports": {
"lit": "https://esm.sh/lit@3.2.1",
"lit/": "https://esm.sh/lit@3.2.1/"
}
}
</script>
<script type="module" src="/ui/vendor/whynot-design/index.js"></script>
</head>
<body>
<wn-top-nav brand="adaptive-pricing" slug="coulomb · observatory">
<div slot="right" class="obs-period" id="period-control">
<wn-eyebrow>Period</wn-eyebrow>
<wn-select id="period-select"></wn-select>
</div>
<wn-tag slot="right" id="liquidity-badge"></wn-tag>
</wn-top-nav>
<div class="wn-app obs-app">
<wn-sidebar id="obs-sidebar">
<wn-sidebar-group label="Observatory">
<wn-sidebar-item icon="activity" active data-section="cost-floor" id="nav-cost-floor">
Cost Floor
</wn-sidebar-item>
<wn-sidebar-item icon="lightbulb" data-section="value-range" id="nav-value-range">
Value Range
</wn-sidebar-item>
<wn-sidebar-item icon="users" data-section="market-price" id="nav-market-price">
Market Price
</wn-sidebar-item>
</wn-sidebar-group>
</wn-sidebar>
<main class="wn-main obs-main">
<wn-page-header id="page-header"></wn-page-header>
<section class="obs-panel obs-panel--active" id="panel-cost-floor" data-section="cost-floor">
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Snapshot</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<div class="obs-metric-grid" id="metric-grid"></div>
</section>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Liquidity &amp; budget</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<wn-banner variant="info" title="Budget position" id="budget-banner">
<p id="budget-caption"></p>
</wn-banner>
<div class="obs-budget-meter" aria-hidden="true">
<div class="obs-budget-meter__fill" id="budget-fill"></div>
</div>
<div class="obs-field-sheet" id="budget-stats"></div>
</section>
<div class="obs-split">
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Monthly net liquidity</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<p class="lead obs-section-note">Member net payments minus infrastructure, by period.</p>
<div class="obs-liquidity-list" id="liquidity-chart"></div>
</section>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Infrastructure stack</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<p class="lead obs-section-note">Domains, hosting, and Stripe reference rates.</p>
<div id="infra-stack"></div>
</section>
</div>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Monthly ledger</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<p class="lead obs-section-note">Computed from expense and payment record tables.</p>
<div class="obs-table-wrap">
<table class="wn-table--native obs-table">
<thead>
<tr>
<th>Period</th>
<th>Members</th>
<th class="obs-num">Gross</th>
<th class="obs-num">Infrastructure</th>
<th class="obs-num">Processing</th>
<th class="obs-num">Net liquidity</th>
</tr>
</thead>
<tbody id="history-body"></tbody>
</table>
</div>
</section>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Pricing model registry</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<div class="obs-table-wrap">
<table class="wn-table--native obs-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Type</th>
<th>Status</th>
</tr>
</thead>
<tbody id="pricing-body"></tbody>
</table>
</div>
</section>
</section>
<section class="obs-panel" id="panel-value-range" data-section="value-range" hidden>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Current position</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<div class="obs-metric-grid" id="value-summary"></div>
</section>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Segment bands</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<p class="lead obs-section-note" id="value-range-notes"></p>
<div class="obs-value-grid" id="value-segments"></div>
</section>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Value drivers</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<div class="obs-field-sheet" id="value-drivers"></div>
</section>
</section>
<section class="obs-panel" id="panel-market-price" data-section="market-price" hidden>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Market span</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<div class="obs-metric-grid" id="market-summary"></div>
</section>
<section class="obs-section">
<div class="obs-section__head">
<wn-eyebrow>Competitive alternatives</wn-eyebrow>
<div class="obs-section__rule"></div>
</div>
<p class="lead obs-section-note" id="market-notes"></p>
<div class="obs-table-wrap">
<table class="wn-table--native obs-table">
<thead>
<tr>
<th>Alternative</th>
<th>Category</th>
<th class="obs-num">Price / mo</th>
<th>Signal</th>
<th>Features</th>
<th>Evidence</th>
</tr>
</thead>
<tbody id="market-body"></tbody>
</table>
</div>
</section>
</section>
<footer class="obs-footer">
<p class="small">
Design:
<a id="design-link" href="#" target="_blank" rel="noreferrer">Claude design share</a>
·
<span class="mono" id="design-ref-label">whynot-design</span>
· totals computed programmatically from ledgers
</p>
</footer>
</main>
</div>
<script src="/ui/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,256 @@
/* Observatory layout — extends whynot-design (Layer 1). No gradients, no shadows on cards. */
body {
background: var(--paper);
}
.obs-app {
min-height: calc(100vh - 56px);
}
.obs-main {
padding: var(--sp-6) var(--sp-5) var(--sp-9);
max-width: 960px;
}
.obs-panel[hidden] {
display: none !important;
}
wn-sidebar-item[data-section] {
cursor: pointer;
}
.obs-metric__value--sm {
font-size: 22px;
}
.obs-value-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--sp-3);
}
.obs-range {
display: grid;
gap: var(--sp-2);
}
.obs-range__labels {
display: flex;
justify-content: space-between;
gap: var(--sp-2);
font-size: 12px;
color: var(--fg-3);
}
.obs-range__current {
color: var(--fg-1);
}
.obs-range__track {
position: relative;
height: 10px;
border: 1px solid var(--border);
background: var(--paper-2);
}
.obs-range__fill {
height: 100%;
background: var(--line-strong);
width: 0;
}
.obs-range__marker {
position: absolute;
top: -3px;
width: 2px;
height: 16px;
background: var(--ink);
transform: translateX(-1px);
}
.obs-period {
display: grid;
gap: 6px;
min-width: 132px;
}
wn-top-nav [slot="right"] {
display: flex;
align-items: end;
gap: var(--sp-3);
}
.obs-section {
margin-bottom: var(--sp-7);
}
.obs-section__head {
display: flex;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-4);
}
.obs-section__rule {
flex: 1;
border-top: 1px solid var(--border-soft);
}
.obs-section-note {
margin: 0 0 var(--sp-4);
max-width: 60ch;
}
.obs-metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--sp-3);
}
.obs-metric__value {
font: 500 28px/1.1 var(--ff-sans);
letter-spacing: -0.02em;
color: var(--fg-1);
font-variant-numeric: tabular-nums;
}
.obs-budget-meter {
height: 8px;
border: 1px solid var(--border);
background: var(--paper-2);
margin-bottom: var(--sp-4);
}
.obs-budget-meter__fill {
height: 100%;
width: 0;
background: var(--ink);
transition: width 0.25s ease;
}
.obs-field-sheet {
border: 1px solid var(--border);
border-radius: var(--r-2);
background: var(--paper);
padding: 0 var(--sp-4);
}
.obs-split {
display: grid;
grid-template-columns: 1.35fr 1fr;
gap: var(--sp-6);
margin-bottom: var(--sp-2);
}
.obs-liquidity-list {
border: 1px solid var(--border);
border-radius: var(--r-2);
background: var(--paper);
padding: 0 var(--sp-4);
}
.obs-liquidity-row {
display: grid;
grid-template-columns: 72px 1fr 88px;
gap: var(--sp-3);
align-items: center;
padding: var(--sp-3) 0;
border-bottom: 1px solid var(--border-soft);
}
.obs-liquidity-row:last-child {
border-bottom: 0;
}
.obs-liquidity-row__period {
font: 400 12px var(--ff-mono);
color: var(--fg-3);
}
.obs-liquidity-row__value {
font: 500 12px var(--ff-mono);
color: var(--fg-1);
text-align: right;
font-variant-numeric: tabular-nums;
}
.obs-liquidity-row__value--neg {
color: var(--fg-2);
}
.obs-liquidity-row__bar-wrap {
height: 10px;
border: 1px solid var(--border-soft);
background: var(--paper-2);
position: relative;
}
.obs-liquidity-row__bar {
position: absolute;
top: 0;
bottom: 0;
background: var(--ink);
}
.obs-liquidity-row__bar--neg {
right: 50%;
background: var(--ink-4);
}
.obs-liquidity-row__bar--pos {
left: 50%;
background: var(--ink);
}
.obs-infra-list {
border: 1px solid var(--border);
border-radius: var(--r-2);
background: var(--paper);
padding: 0 var(--sp-4);
}
.obs-table-wrap {
border: 1px solid var(--border);
border-radius: var(--r-2);
overflow-x: auto;
background: var(--paper);
}
.obs-table {
width: 100%;
margin: 0;
}
.obs-num {
text-align: right;
font-variant-numeric: tabular-nums;
}
.obs-footer {
padding-top: var(--sp-5);
border-top: 1px solid var(--border);
}
.obs-footer a {
color: var(--fg-1);
text-decoration: underline;
text-underline-offset: 2px;
}
wn-banner {
display: block;
margin-bottom: var(--sp-4);
}
@media (max-width: 900px) {
.obs-split {
grid-template-columns: 1fr;
}
wn-top-nav [slot="right"] {
width: 100%;
justify-content: space-between;
}
}

View File

@@ -0,0 +1 @@
9b9f3728937ca308966de9c62accdb00c8cf5b0e

View File

@@ -0,0 +1,273 @@
/* ============================================================
WhyNot Design System — Colors & Type
------------------------------------------------------------
Neutral, mostly black/white. Color is used SPARINGLY — only
one warm accent (annotation yellow) borrowed from the LEGO
brick in the logo. The system favours light grey wireframe
artefacts over heavy fills.
============================================================ */
/* ---------- Webfonts (Google Fonts, see /fonts for offline) ---------- */
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600;700&family=IBM+Plex+Serif:ital,wght@0,400;0,500;1,400&display=swap");
:root {
/* ---------- Base palette: neutrals ---------- */
--ink: #0A0A0A; /* near-black, the only "fill" most of the time */
--ink-2: #1F1F1F;
--ink-3: #5C5C5C;
--ink-4: #8A8A8A;
--ink-5: #B5B5B3; /* placeholder text, wireframe labels */
--line: #E5E5E2; /* default 1px wireframe rule */
--line-strong: #C9C9C5; /* dividers between sections */
--line-soft: #F0F0EC; /* hairline within a card */
--paper: #FFFFFF; /* canvas */
--paper-2: #FAFAF7; /* sheet, dim canvas */
--paper-3: #F4F4EF; /* recessed surface, code block bg */
/* ---------- Foreground / background semantic ---------- */
--fg-1: var(--ink);
--fg-2: var(--ink-3);
--fg-3: var(--ink-4);
--fg-mute: var(--ink-5);
--fg-on-dark: #FAFAF7;
--bg-1: var(--paper);
--bg-2: var(--paper-2);
--bg-3: var(--paper-3);
--bg-invert: var(--ink);
--border: var(--line);
--border-strong: var(--line-strong);
--border-soft: var(--line-soft);
/* ---------- The single accent: annotation yellow ---------- */
/* Lifted from the LEGO brick. Used as highlighter, "draft"
stamp, signal-marker. Never as a button fill. */
--hi: #FFE14A;
--hi-2: #FFD400;
--hi-ink: #1A1500; /* text on yellow */
/* ---------- Status (for prototype lifecycle, signal strength) ---------- */
/* Kept deliberately desaturated so they read as labels, not UI. */
--status-raw: #B5B5B3; /* S0 — no signal */
--status-weak: #8A8A8A; /* S1 — weak signal */
--status-medium: #5C5C5C; /* S2 — medium signal */
--status-strong: #0A0A0A; /* S3 — strong signal */
--status-commercial: #FFD400; /* S4 — commercial */
/* ---------- Type families ---------- */
--ff-sans: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
--ff-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
--ff-serif: "IBM Plex Serif", "Iowan Old Style", Georgia, serif;
/* ---------- Type scale (modular, ~1.2) ---------- */
--fs-xs: 11px;
--fs-sm: 13px;
--fs-base: 15px;
--fs-md: 17px;
--fs-lg: 20px;
--fs-xl: 24px;
--fs-2xl: 32px;
--fs-3xl: 44px;
--fs-4xl: 64px;
--fs-5xl: 96px;
--lh-tight: 1.05;
--lh-snug: 1.25;
--lh-base: 1.5;
--lh-loose: 1.7;
--tr-tight: -0.02em;
--tr-snug: -0.01em;
--tr-base: 0em;
--tr-mono: 0.02em;
--tr-label: 0.08em; /* uppercase eyebrow labels */
/* ---------- Spacing (4px base) ---------- */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 24px;
--sp-6: 32px;
--sp-7: 48px;
--sp-8: 64px;
--sp-9: 96px;
--sp-10: 128px;
/* ---------- Radii — small, mostly square ---------- */
--r-0: 0px;
--r-1: 2px;
--r-2: 4px;
--r-3: 8px;
--r-pill: 999px;
/* ---------- Elevation — almost none. This is a wireframe system. ---------- */
--shadow-0: none;
--shadow-1: 0 1px 0 var(--line);
--shadow-2: 0 1px 0 var(--line-strong);
--shadow-3: 0 4px 12px -6px rgba(10,10,10,0.10);
}
/* ============================================================
Semantic element styles
============================================================ */
html {
font-family: var(--ff-sans);
font-size: var(--fs-base);
line-height: var(--lh-base);
color: var(--fg-1);
background: var(--bg-1);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
font-feature-settings: "ss01", "cv11";
text-wrap: pretty;
}
/* ---------- Headings ---------- */
h1, .h1 {
font: 600 var(--fs-3xl)/var(--lh-tight) var(--ff-sans);
letter-spacing: var(--tr-tight);
margin: 0 0 var(--sp-5);
color: var(--fg-1);
}
h2, .h2 {
font: 500 var(--fs-2xl)/var(--lh-snug) var(--ff-sans);
letter-spacing: var(--tr-snug);
margin: 0 0 var(--sp-4);
}
h3, .h3 {
font: 500 var(--fs-xl)/var(--lh-snug) var(--ff-sans);
letter-spacing: var(--tr-snug);
margin: 0 0 var(--sp-3);
}
h4, .h4 {
font: 500 var(--fs-lg)/var(--lh-snug) var(--ff-sans);
margin: 0 0 var(--sp-2);
}
h5, .h5 {
font: 500 var(--fs-md)/var(--lh-snug) var(--ff-sans);
margin: 0 0 var(--sp-2);
}
/* ---------- Display (for hero / title slides) ---------- */
.display-1 {
font: 300 var(--fs-5xl)/0.95 var(--ff-sans);
letter-spacing: -0.035em;
color: var(--fg-1);
}
.display-2 {
font: 400 var(--fs-4xl)/1.0 var(--ff-sans);
letter-spacing: var(--tr-tight);
}
/* ---------- Body ---------- */
p {
margin: 0 0 var(--sp-4);
line-height: var(--lh-base);
color: var(--fg-1);
}
.lead {
font-size: var(--fs-md);
line-height: 1.55;
color: var(--fg-2);
}
small, .small {
font-size: var(--fs-sm);
color: var(--fg-2);
}
/* ---------- Eyebrow / uppercase labels (very common in this system) ---------- */
.eyebrow,
.label {
font: 500 var(--fs-xs)/1.2 var(--ff-mono);
letter-spacing: var(--tr-label);
text-transform: uppercase;
color: var(--fg-3);
}
/* ---------- Code / mono ---------- */
code, kbd, samp, pre, .mono {
font-family: var(--ff-mono);
font-size: 0.92em;
letter-spacing: var(--tr-mono);
}
code {
background: var(--bg-3);
padding: 1px 6px;
border-radius: var(--r-1);
color: var(--ink-2);
}
pre {
background: var(--bg-3);
border: 1px solid var(--border);
padding: var(--sp-4);
overflow-x: auto;
border-radius: var(--r-2);
font-size: var(--fs-sm);
line-height: var(--lh-snug);
}
pre code { background: none; padding: 0; }
/* ---------- Editorial serif moments ---------- */
.serif { font-family: var(--ff-serif); }
.serif-quote {
font: 400 italic var(--fs-xl)/1.4 var(--ff-serif);
color: var(--fg-2);
}
/* ---------- Links ---------- */
a {
color: var(--fg-1);
text-decoration: underline;
text-decoration-color: var(--border-strong);
text-underline-offset: 3px;
text-decoration-thickness: 1px;
transition: text-decoration-color 120ms ease, color 120ms ease;
}
a:hover {
text-decoration-color: var(--fg-1);
}
/* ---------- HR ---------- */
hr {
border: 0;
border-top: 1px solid var(--border);
margin: var(--sp-5) 0;
}
/* ---------- Highlighter (the one place yellow appears in body copy) ---------- */
mark, .mark {
background: var(--hi);
color: var(--hi-ink);
padding: 0 2px;
}
/* ---------- Tables (used in templates) ---------- */
table {
width: 100%;
border-collapse: collapse;
font-size: var(--fs-sm);
}
th, td {
text-align: left;
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--border);
}
th {
font-weight: 500;
color: var(--fg-2);
font-family: var(--ff-mono);
font-size: var(--fs-xs);
letter-spacing: var(--tr-label);
text-transform: uppercase;
}
/* ---------- Selection ---------- */
::selection { background: var(--hi); color: var(--hi-ink); }

Some files were not shown because too many files have changed in this diff Show More