crudecode-valuation 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. crudecode_valuation-0.2.0/.gitignore +24 -0
  2. crudecode_valuation-0.2.0/LICENSE +21 -0
  3. crudecode_valuation-0.2.0/PKG-INFO +136 -0
  4. crudecode_valuation-0.2.0/README.md +117 -0
  5. crudecode_valuation-0.2.0/crude_valuation/README.md +146 -0
  6. crudecode_valuation-0.2.0/crude_valuation/__init__.py +51 -0
  7. crudecode_valuation-0.2.0/crude_valuation/casefile.py +104 -0
  8. crudecode_valuation-0.2.0/crude_valuation/compose.py +79 -0
  9. crudecode_valuation-0.2.0/crude_valuation/econ/__init__.py +4 -0
  10. crudecode_valuation-0.2.0/crude_valuation/econ/cashflow.py +75 -0
  11. crudecode_valuation-0.2.0/crude_valuation/econ/npv.py +14 -0
  12. crudecode_valuation-0.2.0/crude_valuation/econ/revenue.py +39 -0
  13. crudecode_valuation-0.2.0/crude_valuation/forecast/README.md +142 -0
  14. crudecode_valuation-0.2.0/crude_valuation/forecast/__init__.py +37 -0
  15. crudecode_valuation-0.2.0/crude_valuation/forecast/analogs.py +50 -0
  16. crudecode_valuation-0.2.0/crude_valuation/forecast/curve.py +212 -0
  17. crudecode_valuation-0.2.0/crude_valuation/forecast/diagnostics.py +245 -0
  18. crudecode_valuation-0.2.0/crude_valuation/forecast/examples/__init__.py +2 -0
  19. crudecode_valuation-0.2.0/crude_valuation/forecast/examples/forecast_analogs.py +94 -0
  20. crudecode_valuation-0.2.0/crude_valuation/forecast/examples/forecast_historical.py +50 -0
  21. crudecode_valuation-0.2.0/crude_valuation/forecast/examples/forecast_mixed.py +199 -0
  22. crudecode_valuation-0.2.0/crude_valuation/forecast/forecast.py +84 -0
  23. crudecode_valuation-0.2.0/crude_valuation/forecast/wells.py +152 -0
  24. crudecode_valuation-0.2.0/pyproject.toml +31 -0
@@ -0,0 +1,24 @@
1
+ .env
2
+ .venv/
3
+ sessions/
4
+ logs/
5
+ test_output/
6
+ node_modules/
7
+ __pycache__/
8
+ .DS_Store
9
+ *.pyc
10
+ renderer/src/App.css
11
+ renderer/src/assets/
12
+ renderer/public/icons.svg
13
+ repomix-*.txt
14
+ managed_agents/.cache.json
15
+ managed_agents/output/
16
+
17
+ # Sibling/scratch dirs (not part of MCP server)
18
+ .superpowers/
19
+ planning_notes.md
20
+ .worktrees/
21
+
22
+ # Marker written by deploy.sh on the EC2 host — the SHA the running MCP
23
+ # was last deployed against. Never committed.
24
+ .last-mcp-deployed-sha
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Crude Code
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: crudecode-valuation
3
+ Version: 0.2.0
4
+ Summary: Oil & gas deal valuation toolkit for Crude Code inner agents — forecast, cashflow, NPV.
5
+ Project-URL: Homepage, https://github.com/petroleumPythoneer/crudecode
6
+ Author: Crude Code
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: crudecode-analyst>=0.5.0
15
+ Requires-Dist: numpy>=1.24
16
+ Requires-Dist: pandas>=2.0
17
+ Requires-Dist: scipy>=1.10
18
+ Description-Content-Type: text/markdown
19
+
20
+ # crudecode-valuation
21
+
22
+ Domain package for oil & gas deal valuation. Used by the `valuation_agent`
23
+ inner agent in the EI Plugins MCP server.
24
+
25
+ ## What you get
26
+
27
+ Two subsystems:
28
+
29
+ - **`forecast`** — primitives + diagnostic helpers for building production
30
+ forecasts from analogs, own history, or blends.
31
+ - **`econ`** — revenue → cashflow → NPV given a forecast tuple, interest
32
+ type, and economic assumptions.
33
+
34
+ You compose them yourself. No fused `valuate()` entry point.
35
+
36
+ ## Agent workflow — EXPLORE → DECIDE → EXECUTE
37
+
38
+ Every `/tmp/valuation.py` follows three phases marked by comment headers.
39
+
40
+ ### EXPLORE
41
+
42
+ Load production, find analogs, fit them, and inspect with the three
43
+ diagnostic helpers:
44
+
45
+ ```python
46
+ from crude_valuation import (
47
+ load_production, find_analogs, fit_curve,
48
+ cohort_summary, fit_quality, target_vs_cohort,
49
+ )
50
+
51
+ prod = load_production(WELL_APIS, run_sql)
52
+ analog_apis = find_analogs(ANALOG_FILTER, run_sql, exclude=WELL_APIS)
53
+ analog_curves = []
54
+ for a in analog_apis:
55
+ try:
56
+ analog_prod = load_production([a], run_sql)
57
+ q = np.array(analog_prod["oil_bbl"])
58
+ months = np.arange(len(q), dtype=float)
59
+ meta = peek_well(a, run_sql)
60
+ analog_curves.append(
61
+ fit_curve(months, q, stream="oil", lateral_norm_ft=meta.lateral_ft, b_fixed=0.8)
62
+ )
63
+ except ValueError:
64
+ continue # skip thin / un-fittable analogs
65
+
66
+ print(cohort_summary(analog_curves))
67
+ cohort_median = percentile_curves(analog_curves, pct=0.5, norm_to_lateral_ft=10_000.0)
68
+
69
+ for api in WELL_APIS:
70
+ meta = peek_well(api, run_sql)
71
+ target_prod = load_production([api], run_sql)
72
+ target_q = np.array(target_prod["oil_bbl"])
73
+ target_months = np.arange(len(target_q), dtype=float)
74
+ print(api, target_vs_cohort(cohort_median, target_months, target_q, meta.lateral_ft))
75
+ ```
76
+
77
+ ### DECIDE
78
+
79
+ A free-form comment block. Document the call per well in plain English:
80
+
81
+ ```python
82
+ # === DECIDE ===
83
+ # Cohort: 15 analogs after dropping 3 bound-riders. b median 0.92, IQR 0.31.
84
+ # Target 42-329-12345 (Wolfcamp A, 9,400 ft):
85
+ # 36 months observed, qi_ratio 1.04 -> in line with cohort
86
+ # Choice: pdp; fit own decline with b=0.9 (cohort median, dropped outliers)
87
+ # Target 42-329-12999 (PERMITTED, 10,200 ft):
88
+ # no history; planned spud 2026-08
89
+ # Choice: pure_analog with cohort curve at target lateral
90
+ ```
91
+
92
+ ### EXECUTE
93
+
94
+ Build Forecasts inline per-well — no router, no module constants:
95
+
96
+ ```python
97
+ # === EXECUTE ===
98
+ forecasts_oil = []
99
+ forecasts_gas = []
100
+ for api, choice in CHOICES.items():
101
+ if choice["strategy"] == "pdp":
102
+ forecasts_oil.append(_build_pdp(api, b=choice["b"], stream="oil", ...))
103
+ elif choice["strategy"] == "thin_blend":
104
+ forecasts_oil.append(_build_thin(api, cohort_median_oil, ...))
105
+ else: # pure_analog
106
+ forecasts_oil.append(_build_pure_analog(api, cohort_median_oil, ...))
107
+ # same for gas
108
+
109
+ # Econ + persist
110
+ revenue = compute_gross_revenue(forecasts_oil, forecasts_gas, price_deck=...)
111
+ cashflow = compute_net_cashflow(revenue, interest_type=INTEREST_TYPE, ...)
112
+ npv_dict = npv(cashflow, discount_rates=[0.08, 0.10, 0.12, 0.15])
113
+
114
+ spec = build_minimal_briefing(headline=..., commentary=..., npv=npv_dict, ...)
115
+ persist(spec, persist_url="$persist_url")
116
+ ```
117
+
118
+ Commentary must reference at least one concrete diagnostic you looked at.
119
+
120
+ ## Playbooks
121
+
122
+ `crude_valuation/forecast/examples/` ships annotated playbooks showing the
123
+ EXPLORE → DECIDE → EXECUTE pattern end-to-end. Today only `forecast_mixed.py`
124
+ is in the new playbook form; the other two are still recipe-style.
125
+
126
+ ## Diagnostic helpers in detail
127
+
128
+ - `cohort_summary(curves)` — distribution stats per fitted parameter (qi, di,
129
+ b, terminal, lateral). Plus `notes` flagging fits that rode the b bound.
130
+ - `fit_quality(curve, months, q)` — R^2, RMSE, bound-riding flags, max
131
+ residual %, early/late signed-% bias windows for a single fit.
132
+ - `target_vs_cohort(cohort, target_months, target_q, target_lateral_ft)` —
133
+ qi_ratio of target to cohort, peak detection, per-month signed-% residual
134
+ vs cohort placed at target lateral.
135
+
136
+ None of these decide anything. The agent reads them and chooses.
@@ -0,0 +1,117 @@
1
+ # crudecode-valuation
2
+
3
+ Domain package for oil & gas deal valuation. Used by the `valuation_agent`
4
+ inner agent in the EI Plugins MCP server.
5
+
6
+ ## What you get
7
+
8
+ Two subsystems:
9
+
10
+ - **`forecast`** — primitives + diagnostic helpers for building production
11
+ forecasts from analogs, own history, or blends.
12
+ - **`econ`** — revenue → cashflow → NPV given a forecast tuple, interest
13
+ type, and economic assumptions.
14
+
15
+ You compose them yourself. No fused `valuate()` entry point.
16
+
17
+ ## Agent workflow — EXPLORE → DECIDE → EXECUTE
18
+
19
+ Every `/tmp/valuation.py` follows three phases marked by comment headers.
20
+
21
+ ### EXPLORE
22
+
23
+ Load production, find analogs, fit them, and inspect with the three
24
+ diagnostic helpers:
25
+
26
+ ```python
27
+ from crude_valuation import (
28
+ load_production, find_analogs, fit_curve,
29
+ cohort_summary, fit_quality, target_vs_cohort,
30
+ )
31
+
32
+ prod = load_production(WELL_APIS, run_sql)
33
+ analog_apis = find_analogs(ANALOG_FILTER, run_sql, exclude=WELL_APIS)
34
+ analog_curves = []
35
+ for a in analog_apis:
36
+ try:
37
+ analog_prod = load_production([a], run_sql)
38
+ q = np.array(analog_prod["oil_bbl"])
39
+ months = np.arange(len(q), dtype=float)
40
+ meta = peek_well(a, run_sql)
41
+ analog_curves.append(
42
+ fit_curve(months, q, stream="oil", lateral_norm_ft=meta.lateral_ft, b_fixed=0.8)
43
+ )
44
+ except ValueError:
45
+ continue # skip thin / un-fittable analogs
46
+
47
+ print(cohort_summary(analog_curves))
48
+ cohort_median = percentile_curves(analog_curves, pct=0.5, norm_to_lateral_ft=10_000.0)
49
+
50
+ for api in WELL_APIS:
51
+ meta = peek_well(api, run_sql)
52
+ target_prod = load_production([api], run_sql)
53
+ target_q = np.array(target_prod["oil_bbl"])
54
+ target_months = np.arange(len(target_q), dtype=float)
55
+ print(api, target_vs_cohort(cohort_median, target_months, target_q, meta.lateral_ft))
56
+ ```
57
+
58
+ ### DECIDE
59
+
60
+ A free-form comment block. Document the call per well in plain English:
61
+
62
+ ```python
63
+ # === DECIDE ===
64
+ # Cohort: 15 analogs after dropping 3 bound-riders. b median 0.92, IQR 0.31.
65
+ # Target 42-329-12345 (Wolfcamp A, 9,400 ft):
66
+ # 36 months observed, qi_ratio 1.04 -> in line with cohort
67
+ # Choice: pdp; fit own decline with b=0.9 (cohort median, dropped outliers)
68
+ # Target 42-329-12999 (PERMITTED, 10,200 ft):
69
+ # no history; planned spud 2026-08
70
+ # Choice: pure_analog with cohort curve at target lateral
71
+ ```
72
+
73
+ ### EXECUTE
74
+
75
+ Build Forecasts inline per-well — no router, no module constants:
76
+
77
+ ```python
78
+ # === EXECUTE ===
79
+ forecasts_oil = []
80
+ forecasts_gas = []
81
+ for api, choice in CHOICES.items():
82
+ if choice["strategy"] == "pdp":
83
+ forecasts_oil.append(_build_pdp(api, b=choice["b"], stream="oil", ...))
84
+ elif choice["strategy"] == "thin_blend":
85
+ forecasts_oil.append(_build_thin(api, cohort_median_oil, ...))
86
+ else: # pure_analog
87
+ forecasts_oil.append(_build_pure_analog(api, cohort_median_oil, ...))
88
+ # same for gas
89
+
90
+ # Econ + persist
91
+ revenue = compute_gross_revenue(forecasts_oil, forecasts_gas, price_deck=...)
92
+ cashflow = compute_net_cashflow(revenue, interest_type=INTEREST_TYPE, ...)
93
+ npv_dict = npv(cashflow, discount_rates=[0.08, 0.10, 0.12, 0.15])
94
+
95
+ spec = build_minimal_briefing(headline=..., commentary=..., npv=npv_dict, ...)
96
+ persist(spec, persist_url="$persist_url")
97
+ ```
98
+
99
+ Commentary must reference at least one concrete diagnostic you looked at.
100
+
101
+ ## Playbooks
102
+
103
+ `crude_valuation/forecast/examples/` ships annotated playbooks showing the
104
+ EXPLORE → DECIDE → EXECUTE pattern end-to-end. Today only `forecast_mixed.py`
105
+ is in the new playbook form; the other two are still recipe-style.
106
+
107
+ ## Diagnostic helpers in detail
108
+
109
+ - `cohort_summary(curves)` — distribution stats per fitted parameter (qi, di,
110
+ b, terminal, lateral). Plus `notes` flagging fits that rode the b bound.
111
+ - `fit_quality(curve, months, q)` — R^2, RMSE, bound-riding flags, max
112
+ residual %, early/late signed-% bias windows for a single fit.
113
+ - `target_vs_cohort(cohort, target_months, target_q, target_lateral_ft)` —
114
+ qi_ratio of target to cohort, peak detection, per-month signed-% residual
115
+ vs cohort placed at target lateral.
116
+
117
+ None of these decide anything. The agent reads them and chooses.
@@ -0,0 +1,146 @@
1
+ # crude_valuation
2
+
3
+ In-package cookbook the inner valuation_agent reads at runtime. Walks
4
+ through the three-phase workflow and the diagnostic helpers.
5
+
6
+ The package has two subsystems:
7
+
8
+ - **`forecast/`** — produce `(forecasts_oil, forecasts_gas)`. Three
9
+ diagnostic helpers (`cohort_summary`, `fit_quality`,
10
+ `target_vs_cohort`) let you inspect the cohort and target wells before
11
+ committing to forecast parameters.
12
+ - **`econ/`** — turn forecasts into money. Revenue → cashflow → NPV.
13
+
14
+ Forecast and econ are deliberately separate. The forecast step's
15
+ deliverable is `(forecasts_oil, forecasts_gas)` — that tuple is the input
16
+ to the econ step. There is no fused entry point.
17
+
18
+ ## Imports (flat)
19
+
20
+ ```python
21
+ from crude_valuation import (
22
+ # Forecast types and primitives
23
+ DeclineCurve, Forecast, ForecastProvenance,
24
+ fit_curve, percentile_curves, blend_curves, curve_rate,
25
+ project, aggregate,
26
+ peek_well, load_production, find_analogs,
27
+ # Diagnostics
28
+ cohort_summary, fit_quality, target_vs_cohort,
29
+ CohortSummary, FitQuality, TargetComparison, ParamStats,
30
+ # Econ
31
+ compute_gross_revenue, compute_net_cashflow, npv,
32
+ # Briefing assembly
33
+ build_minimal_briefing,
34
+ )
35
+ ```
36
+
37
+ ## Workflow — EXPLORE → DECIDE → EXECUTE
38
+
39
+ Every `/tmp/valuation.py` you write is structured into three phases marked
40
+ by comment headers. You think like a petroleum engineer: inspect, decide,
41
+ execute. Document the calls you made in plain English so a reader of the
42
+ briefing can audit them.
43
+
44
+ ### EXPLORE — load data, fit analogs, print diagnostics
45
+
46
+ ```python
47
+ import numpy as np
48
+ from crude_valuation import (
49
+ load_production, find_analogs, fit_curve, peek_well,
50
+ percentile_curves,
51
+ cohort_summary, fit_quality, target_vs_cohort,
52
+ )
53
+
54
+ prod = load_production(WELL_APIS, run_sql)
55
+ analog_apis = find_analogs(ANALOG_FILTER, run_sql, exclude=WELL_APIS)
56
+
57
+ analog_curves = []
58
+ for a in analog_apis:
59
+ try:
60
+ analog_prod = load_production([a], run_sql)
61
+ q = np.array(analog_prod["oil_bbl"])
62
+ months = np.arange(len(q), dtype=float)
63
+ meta = peek_well(a, run_sql)
64
+ analog_curves.append(
65
+ fit_curve(months, q, stream="oil", lateral_norm_ft=meta.lateral_ft, b_fixed=0.8)
66
+ )
67
+ except ValueError:
68
+ continue # skip thin / un-fittable analogs
69
+
70
+ print(cohort_summary(analog_curves)) # AGENT READS THIS
71
+ cohort_median = percentile_curves(analog_curves, pct=0.5, norm_to_lateral_ft=10_000.0)
72
+
73
+ for api in WELL_APIS:
74
+ meta = peek_well(api, run_sql)
75
+ target_prod = load_production([api], run_sql)
76
+ target_q = np.array(target_prod["oil_bbl"])
77
+ target_months = np.arange(len(target_q), dtype=float)
78
+ print(api, target_vs_cohort(cohort_median, target_months, target_q, meta.lateral_ft))
79
+ ```
80
+
81
+ For PDP candidates (wells you'll fit on their own history), also print
82
+ `fit_quality(own_curve, months, q)` so you can gate fits that rode the
83
+ b bound.
84
+
85
+ ### DECIDE — write a comment block per well
86
+
87
+ ```python
88
+ # === DECIDE ===
89
+ # Cohort: 15 analogs after dropping 3 b-upper-bound riders. b median 0.92
90
+ # (IQR 0.31). qi median 14,200 bbl/mo at 9,800 ft lateral.
91
+ #
92
+ # Target 42-329-12345 (Wolfcamp A, 9,400 ft):
93
+ # 36 months observed, qi_ratio 1.04 -> in line with cohort
94
+ # Choice: pdp; fit own decline with b=0.9 (cohort median, dropped outliers)
95
+ #
96
+ # Target 42-329-12999 (PERMITTED, 10,200 ft):
97
+ # no history; planned spud 2026-08
98
+ # Choice: pure_analog with cohort curve at target lateral
99
+ ```
100
+
101
+ ### EXECUTE — inline the per-well calls, then run econ
102
+
103
+ No router constants. No `THIN_HISTORY_THRESHOLD`. No `B_FIXED`. Each
104
+ well's strategy is whatever the DECIDE block said.
105
+
106
+ ```python
107
+ forecasts_oil, forecasts_gas = [], []
108
+ for api, choice in CHOICES.items():
109
+ if choice["strategy"] == "pdp":
110
+ forecasts_oil.append(_build_pdp(api, b=choice["b"], stream="oil", ...))
111
+ elif choice["strategy"] == "thin_blend":
112
+ forecasts_oil.append(_build_thin(api, cohort_median_oil, ...))
113
+ else: # pure_analog
114
+ forecasts_oil.append(_build_pure_analog(api, cohort_median_oil, ...))
115
+ # same for gas
116
+
117
+ revenue = compute_gross_revenue(forecasts_oil, forecasts_gas, price_deck=...)
118
+ cashflow = compute_net_cashflow(revenue, interest_type=INTEREST_TYPE, ...)
119
+ npv_dict = npv(cashflow, discount_rates=[0.08, 0.10, 0.12, 0.15])
120
+
121
+ spec = build_minimal_briefing(headline=..., commentary=..., npv=npv_dict, ...)
122
+ persist(spec, persist_url="$persist_url")
123
+ ```
124
+
125
+ Commentary must reference at least one concrete diagnostic you looked at
126
+ (e.g. "cohort of 14 analogs, b median 0.92 with IQR 0.31; target's first
127
+ 4 months at 71% of cohort qi suggests below-average completion").
128
+
129
+ ## Playbooks
130
+
131
+ `crude_valuation/forecast/examples/` ships annotated playbooks. Today
132
+ only `forecast_mixed.py` is in the new playbook form; the other two are
133
+ still recipe-style and will be migrated.
134
+
135
+ ## Diagnostic helpers in detail
136
+
137
+ - `cohort_summary(curves)` — distribution stats per fitted parameter
138
+ (qi, di, b, terminal, lateral). Plus `notes` flagging fits that rode
139
+ the b bound.
140
+ - `fit_quality(curve, months, q)` — R^2, RMSE, bound-riding flags, max
141
+ residual %, early/late signed-% bias windows for a single fit.
142
+ - `target_vs_cohort(cohort, target_months, target_q, target_lateral_ft)` —
143
+ qi_ratio of target to cohort, peak detection, per-month signed-%
144
+ residual vs cohort placed at target lateral.
145
+
146
+ None of these decide anything. You read them and choose.
@@ -0,0 +1,51 @@
1
+ """crude-valuation — valuation toolkit for the inner valuation_agent.
2
+
3
+ Wraps crude_analyst (the base briefing platform) and adds domain math.
4
+ The agent only ever imports from crude_valuation; crude_analyst is invisible.
5
+ """
6
+ from crude_analyst import * # noqa: F401, F403
7
+
8
+ # Econ subsystem
9
+ from crude_valuation.econ import ( # noqa: F401
10
+ compute_gross_revenue,
11
+ compute_net_cashflow,
12
+ npv,
13
+ )
14
+
15
+ # Shared utilities
16
+ from crude_valuation.compose import build_minimal_briefing # noqa: F401
17
+
18
+ # Forecast subsystem — flat re-exports for the agent
19
+ from crude_valuation.forecast import ( # noqa: F401
20
+ # Types
21
+ CohortSummary,
22
+ CurveProvenance,
23
+ DeclineCurve,
24
+ FitQuality,
25
+ Forecast,
26
+ ForecastProvenance,
27
+ ParamStats,
28
+ TargetComparison,
29
+ WellMeta,
30
+ # Primitives
31
+ fit_curve,
32
+ percentile_curves,
33
+ blend_curves,
34
+ curve_rate,
35
+ # Diagnostics
36
+ cohort_summary,
37
+ fit_quality,
38
+ target_vs_cohort,
39
+ # Projection + aggregation
40
+ project,
41
+ aggregate,
42
+ # DB helpers
43
+ peek_well,
44
+ load_production,
45
+ load_well_status,
46
+ resolve_asset_list,
47
+ find_analogs,
48
+ # Errors
49
+ AnalogError,
50
+ WellsError,
51
+ )
@@ -0,0 +1,104 @@
1
+ """Case file parsing for the valuation agent.
2
+
3
+ Validates the JSON contract the MCP tool sends, returns a typed CaseFile
4
+ the rest of the package can read without re-checking shapes.
5
+ """
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+
10
+ class CaseFileError(ValueError):
11
+ """Raised when the case file fails schema validation."""
12
+
13
+
14
+ @dataclass
15
+ class CaseFile:
16
+ interest_type: str # "wi" | "minerals"
17
+ interest: dict # {wi_pct, nri_pct} or {decimal}
18
+ asset_list: dict # {well_apis: [...]} XOR {filter_sql: "..."}
19
+ economics_overrides: dict = field(default_factory=dict)
20
+ transcript: list = field(default_factory=list)
21
+ queries_run: list = field(default_factory=list)
22
+ handoff: str = ""
23
+ source_documents: list = field(default_factory=list)
24
+
25
+
26
+ def parse_case_file(body: dict) -> CaseFile:
27
+ """Validate + parse the case file body. Raises CaseFileError on any failure."""
28
+ if not isinstance(body, dict):
29
+ raise CaseFileError(f"case file must be an object, got {type(body).__name__}")
30
+
31
+ interest_type = body.get("interest_type")
32
+ if interest_type not in ("wi", "minerals"):
33
+ raise CaseFileError(
34
+ f"interest_type must be 'wi' or 'minerals', got {interest_type!r}"
35
+ )
36
+
37
+ interest = body.get("interest")
38
+ if not isinstance(interest, dict):
39
+ raise CaseFileError("interest must be an object")
40
+ if interest_type == "wi":
41
+ for k in ("wi_pct", "nri_pct"):
42
+ if k not in interest:
43
+ raise CaseFileError(f"interest.{k} is required for interest_type='wi'")
44
+ v = interest[k]
45
+ if not isinstance(v, (int, float)) or not (0.0 <= v <= 1.0):
46
+ raise CaseFileError(f"interest.{k} must be in [0, 1], got {v!r}")
47
+ else: # minerals
48
+ if "decimal" not in interest:
49
+ raise CaseFileError("interest.decimal is required for interest_type='minerals'")
50
+ v = interest["decimal"]
51
+ if not isinstance(v, (int, float)) or not (0.0 <= v <= 1.0):
52
+ raise CaseFileError(f"interest.decimal must be in [0, 1], got {v!r}")
53
+
54
+ asset_list = body.get("asset_list")
55
+ if not isinstance(asset_list, dict):
56
+ raise CaseFileError("asset_list must be an object")
57
+ has_apis = bool(asset_list.get("well_apis"))
58
+ has_sql = bool(asset_list.get("filter_sql"))
59
+ if has_apis and has_sql:
60
+ raise CaseFileError("asset_list must have exactly one of well_apis or filter_sql, not both")
61
+ if not has_apis and not has_sql:
62
+ raise CaseFileError("asset_list must have exactly one of well_apis or filter_sql")
63
+ if has_apis and not all(isinstance(a, str) for a in asset_list["well_apis"]):
64
+ raise CaseFileError("asset_list.well_apis must be a list of strings")
65
+ if has_sql and not isinstance(asset_list["filter_sql"], str):
66
+ raise CaseFileError("asset_list.filter_sql must be a string")
67
+
68
+ handoff = body.get("handoff")
69
+ if not isinstance(handoff, str) or not handoff.strip():
70
+ raise CaseFileError("handoff is required and must be a non-empty string")
71
+
72
+ transcript = body.get("transcript")
73
+ if not isinstance(transcript, list) or len(transcript) == 0:
74
+ raise CaseFileError("transcript is required and must be a non-empty list")
75
+ for i, turn in enumerate(transcript):
76
+ if not isinstance(turn, dict):
77
+ raise CaseFileError(f"transcript[{i}] must be an object")
78
+ if turn.get("role") not in ("user", "assistant"):
79
+ raise CaseFileError(f"transcript[{i}].role must be 'user' or 'assistant'")
80
+ if not isinstance(turn.get("content"), str):
81
+ raise CaseFileError(f"transcript[{i}].content must be a string")
82
+
83
+ queries_run = body.get("queries_run", [])
84
+ if not isinstance(queries_run, list):
85
+ raise CaseFileError("queries_run must be a list")
86
+
87
+ economics_overrides = body.get("economics_overrides", {}) or {}
88
+ if not isinstance(economics_overrides, dict):
89
+ raise CaseFileError("economics_overrides must be an object")
90
+
91
+ source_documents = body.get("source_documents", []) or []
92
+ if not isinstance(source_documents, list):
93
+ raise CaseFileError("source_documents must be a list")
94
+
95
+ return CaseFile(
96
+ interest_type=interest_type,
97
+ interest=interest,
98
+ asset_list=asset_list,
99
+ economics_overrides=economics_overrides,
100
+ transcript=transcript,
101
+ queries_run=queries_run,
102
+ handoff=handoff.strip(),
103
+ source_documents=source_documents,
104
+ )
@@ -0,0 +1,79 @@
1
+ """Build a minimal Slice-1 briefing spec from valuation results.
2
+
3
+ Slice 1 is PV-only and commentary-only:
4
+
5
+ - PV-only — no IRR, no payout, no MOIC, no sensitivity. Those land in a later
6
+ slice; this slice ships discount-rate NPV values and the methodology footer.
7
+ - Commentary-only — no callout widget. The base `callout` is designed to render
8
+ a value from a SQL row, not a Python-side literal; using it for a static
9
+ headline NPV would require a crude_analyst widget change we're deferring.
10
+ Instead the headline NPV lives in the commentary text body.
11
+ """
12
+ import math
13
+ from crude_analyst import briefing, section, commentary
14
+
15
+
16
+ def _fmt_money(x: float) -> str:
17
+ """Render dollar values as $X.YM, $XK, etc. Negative shown as -$X.YM."""
18
+ if not math.isfinite(x):
19
+ return "n/a"
20
+ sign = "-" if x < 0 else ""
21
+ a = abs(x)
22
+ if a >= 1e9:
23
+ return f"{sign}${a/1e9:.2f}B"
24
+ if a >= 1e6:
25
+ return f"{sign}${a/1e6:.2f}M"
26
+ if a >= 1e3:
27
+ return f"{sign}${a/1e3:.1f}K"
28
+ return f"{sign}${a:.0f}"
29
+
30
+
31
+ def _headline_rate(npv_by_rate: dict[float, float]) -> float:
32
+ """Pick the headline discount rate — PV10 if present, else the lowest rate."""
33
+ if 0.10 in npv_by_rate:
34
+ return 0.10
35
+ return min(npv_by_rate.keys())
36
+
37
+
38
+ def build_minimal_briefing(
39
+ *,
40
+ npv_by_rate: dict[float, float],
41
+ n_wells: int,
42
+ interest_type: str,
43
+ methodology_notes: str,
44
+ ) -> dict:
45
+ """Assemble the Slice 1 briefing: one commentary widget with PV results."""
46
+ headline_rate = _headline_rate(npv_by_rate)
47
+ headline_npv = npv_by_rate[headline_rate]
48
+ headline_label = f"PV{int(headline_rate*100)}"
49
+ interest_label = "minerals" if interest_type == "minerals" else "working-interest"
50
+
51
+ lines = [
52
+ f"{interest_label.capitalize()} valuation across {n_wells} producing well(s).",
53
+ f"{headline_label}: {_fmt_money(headline_npv)}.",
54
+ ]
55
+ if len(npv_by_rate) > 1:
56
+ other = ", ".join(
57
+ f"PV{int(r*100)}: {_fmt_money(v)}"
58
+ for r, v in sorted(npv_by_rate.items())
59
+ if r != headline_rate
60
+ )
61
+ lines.append(f"Other rates — {other}.")
62
+ lines.append("")
63
+ lines.append(f"Methodology: {methodology_notes}")
64
+
65
+ spec = briefing(
66
+ headline=f"{headline_label} {_fmt_money(headline_npv)} on {n_wells} {interest_label} well(s).",
67
+ tldr=f"{n_wells}-well {interest_type} valuation; headline {headline_label} = {_fmt_money(headline_npv)}.",
68
+ sections=[
69
+ section(
70
+ label="01 · Result",
71
+ layout="full-width",
72
+ widgets=[commentary(text="\n".join(lines))],
73
+ ),
74
+ ],
75
+ )
76
+ spec["headline_npv"] = {
77
+ str(round(rate * 100)): value for rate, value in npv_by_rate.items()
78
+ }
79
+ return spec
@@ -0,0 +1,4 @@
1
+ """Economics: revenue → cashflow → NPV."""
2
+ from crude_valuation.econ.revenue import compute_gross_revenue # noqa: F401
3
+ from crude_valuation.econ.cashflow import compute_net_cashflow # noqa: F401
4
+ from crude_valuation.econ.npv import npv # noqa: F401