gea-program 0.1.0__py3-none-any.whl

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 (163) hide show
  1. gea/BENCH_TEST_PROTOCOL.md +97 -0
  2. gea/IMPORT_RECORD.md +61 -0
  3. gea/__init__.py +160 -0
  4. gea/__main__.py +661 -0
  5. gea/acceptance_tests.py +1315 -0
  6. gea/accuracy_statement.py +181 -0
  7. gea/alarm_engine.py +307 -0
  8. gea/bench.py +144 -0
  9. gea/blind_harness.py +108 -0
  10. gea/case_study.py +229 -0
  11. gea/catalog/acex_lomonosov_age_depth_model.provenance.json +23 -0
  12. gea/catalog/acex_lomonosov_age_depth_model.txt +28 -0
  13. gea/catalog/agassiz77_canada_temperature.csv +68 -0
  14. gea/catalog/agassiz77_canada_temperature.provenance.json +17 -0
  15. gea/catalog/barbados_110_consolidation.provenance.json +24 -0
  16. gea/catalog/barbados_110_consolidation.txt +91 -0
  17. gea/catalog/bengal_u1452_grain_size.provenance.json +20 -0
  18. gea/catalog/bengal_u1452_grain_size.txt +252 -0
  19. gea/catalog/blake_164_methane_isotopes.provenance.json +22 -0
  20. gea/catalog/blake_164_methane_isotopes.txt +68 -0
  21. gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +23 -0
  22. gea/catalog/chicxulub_m0077a_pwave_velocity.txt +735 -0
  23. gea/catalog/collingwood_1_28_ks_complete.las +128 -0
  24. gea/catalog/collingwood_1_28_ks_complete.provenance.json +17 -0
  25. gea/catalog/costa_rica_odp_friction_envelope.provenance.json +24 -0
  26. gea/catalog/costa_rica_odp_friction_envelope.txt +57 -0
  27. gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +21 -0
  28. gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +94 -0
  29. gea/catalog/dsdp_504b_physical_properties.provenance.json +24 -0
  30. gea/catalog/dsdp_504b_physical_properties.txt +82 -0
  31. gea/catalog/dsdp_504b_sound_velocity.provenance.json +21 -0
  32. gea/catalog/dsdp_504b_sound_velocity.txt +81 -0
  33. gea/catalog/elgygytgyn_5011_turbidites.provenance.json +20 -0
  34. gea/catalog/elgygytgyn_5011_turbidites.txt +193 -0
  35. gea/catalog/epica_domec_co2_800kyr.provenance.json +21 -0
  36. gea/catalog/epica_domec_co2_800kyr.txt +265 -0
  37. gea/catalog/fram_909_organic_petrography.provenance.json +22 -0
  38. gea/catalog/fram_909_organic_petrography.txt +40 -0
  39. gea/catalog/gbr_325_coral_uth_ages.provenance.json +23 -0
  40. gea/catalog/gbr_325_coral_uth_ages.txt +71 -0
  41. gea/catalog/gisp2_greenland_temperature.csv +599 -0
  42. gea/catalog/gisp2_greenland_temperature.provenance.json +17 -0
  43. gea/catalog/gom_308_t2p_insitu.provenance.json +21 -0
  44. gea/catalog/gom_308_t2p_insitu.txt +40 -0
  45. gea/catalog/guaymas_385_dom_d13c.provenance.json +21 -0
  46. gea/catalog/guaymas_385_dom_d13c.txt +103 -0
  47. gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +26 -0
  48. gea/catalog/hikurangi_u1520_friction_insitu.txt +74 -0
  49. gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +22 -0
  50. gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +60 -0
  51. gea/catalog/iodp_u1324_pore_pressure.provenance.json +22 -0
  52. gea/catalog/iodp_u1324_pore_pressure.txt +42 -0
  53. gea/catalog/jfast_c0019_slow_slip_events.provenance.json +28 -0
  54. gea/catalog/jfast_c0019_slow_slip_events.txt +35 -0
  55. gea/catalog/kennetcook_2_p129_excerpt.las +139 -0
  56. gea/catalog/kennetcook_2_p129_excerpt.provenance.json +17 -0
  57. gea/catalog/ktb_hb_bhgm_density.dat +227 -0
  58. gea/catalog/ktb_hb_bhgm_density.provenance.json +22 -0
  59. gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +20 -0
  60. gea/catalog/ktb_hb_complog_6020_excerpt.txt +72 -0
  61. gea/catalog/ktb_hb_hlog246_temperature.dat +1636 -0
  62. gea/catalog/ktb_hb_hlog246_temperature.provenance.json +22 -0
  63. gea/catalog/ktb_hb_rockmech_compress.dat +33 -0
  64. gea/catalog/ktb_hb_rockmech_compress.provenance.json +22 -0
  65. gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +2817 -0
  66. gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +20 -0
  67. gea/catalog/ktb_vb_rockmech_compress.dat +125 -0
  68. gea/catalog/ktb_vb_rockmech_compress.provenance.json +22 -0
  69. gea/catalog/ktb_vb_vlog251_temperature.dat +1127 -0
  70. gea/catalog/ktb_vb_vlog251_temperature.provenance.json +24 -0
  71. gea/catalog/l06_06_nl_survey.csv +201 -0
  72. gea/catalog/l06_06_nl_survey.provenance.json +17 -0
  73. gea/catalog/l07_01_nl_excerpt.las +90 -0
  74. gea/catalog/l07_01_nl_excerpt.provenance.json +17 -0
  75. gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +21 -0
  76. gea/catalog/mariana_1200_serpentinite_geochem.txt +63 -0
  77. gea/catalog/med_160_sapropels.provenance.json +22 -0
  78. gea/catalog/med_160_sapropels.txt +43 -0
  79. gea/catalog/nankai_megasplay_shear_strength.provenance.json +26 -0
  80. gea/catalog/nankai_megasplay_shear_strength.txt +42 -0
  81. gea/catalog/odp_1027b_thermal_conductivity.provenance.json +21 -0
  82. gea/catalog/odp_1027b_thermal_conductivity.txt +52 -0
  83. gea/catalog/odp_1027c_cork_temperature.provenance.json +20 -0
  84. gea/catalog/odp_1027c_cork_temperature.txt +26 -0
  85. gea/catalog/odp_1165b_thermal_conductivity.provenance.json +22 -0
  86. gea/catalog/odp_1165b_thermal_conductivity.txt +102 -0
  87. gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +27 -0
  88. gea/catalog/odp_1274a_mantle_peridotite_mad.txt +44 -0
  89. gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +27 -0
  90. gea/catalog/odp_504b_dike_elastic_moduli.txt +85 -0
  91. gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +19 -0
  92. gea/catalog/odp_504b_leg137_borehole_fluids.txt +68 -0
  93. gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +28 -0
  94. gea/catalog/odp_735b_gabbro_elastic_moduli.txt +127 -0
  95. gea/catalog/peru_201_sulfate_reduction.provenance.json +22 -0
  96. gea/catalog/peru_201_sulfate_reduction.txt +322 -0
  97. gea/catalog/scorpio_e1_sa_excerpt.las +113 -0
  98. gea/catalog/scorpio_e1_sa_excerpt.provenance.json +17 -0
  99. gea/catalog/sumatra_362_cohesion.provenance.json +21 -0
  100. gea/catalog/sumatra_362_cohesion.txt +38 -0
  101. gea/catalog/university_6_17_no1_tx_excerpt.las +119 -0
  102. gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +17 -0
  103. gea/catalog/ursa_308_xrd_mineralogy.provenance.json +21 -0
  104. gea/catalog/ursa_308_xrd_mineralogy.txt +46 -0
  105. gea/catalog/volve_15_9_19_sr_excerpt.las +183 -0
  106. gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +16 -0
  107. gea/catalog/volve_15_9_19a_core_excerpt.csv +88 -0
  108. gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +18 -0
  109. gea/catalog/volve_f12_f14_production_excerpt.csv +167 -0
  110. gea/catalog/volve_f12_f14_production_excerpt.provenance.json +21 -0
  111. gea/catalog/walvis_208_petm_carbonate.provenance.json +21 -0
  112. gea/catalog/walvis_208_petm_carbonate.txt +268 -0
  113. gea/catalog/woodlark_1109_rock_eval.provenance.json +23 -0
  114. gea/catalog/woodlark_1109_rock_eval.txt +30 -0
  115. gea/cli.py +125 -0
  116. gea/client_reports.py +943 -0
  117. gea/config_versioning.py +133 -0
  118. gea/correlation.py +155 -0
  119. gea/dashboard.py +390 -0
  120. gea/deviation.py +70 -0
  121. gea/downhole_engine.py +395 -0
  122. gea/drift_monitor.py +310 -0
  123. gea/earth_model.py +230 -0
  124. gea/example_register_map.json +14 -0
  125. gea/fat_sat.py +68 -0
  126. gea/follower.py +98 -0
  127. gea/forward_model.py +139 -0
  128. gea/gamma.py +176 -0
  129. gea/gauge_specs.py +112 -0
  130. gea/gravity_reference.py +116 -0
  131. gea/inverse_engine.py +215 -0
  132. gea/matplotlib_demo.py +85 -0
  133. gea/modbus.py +229 -0
  134. gea/model_card.py +248 -0
  135. gea/operator_app.py +442 -0
  136. gea/ports.py +340 -0
  137. gea/profile_catalog.py +773 -0
  138. gea/project.py +213 -0
  139. gea/qt6_downhole_app.py +144 -0
  140. gea/quartz_hpht_extension.py +152 -0
  141. gea/reconciler.py +206 -0
  142. gea/rock_inventory.py +404 -0
  143. gea/sample_record.py +430 -0
  144. gea/sample_well_profile.csv +15 -0
  145. gea/sbom.py +116 -0
  146. gea/segy.py +181 -0
  147. gea/service_life.py +173 -0
  148. gea/shell.py +107 -0
  149. gea/sla_report.py +199 -0
  150. gea/store_forward.py +234 -0
  151. gea/strata_join.py +186 -0
  152. gea/survey_cmd.py +264 -0
  153. gea/survey_view.py +138 -0
  154. gea/telemetry.py +306 -0
  155. gea/tool_library.py +260 -0
  156. gea/well_assembler.py +457 -0
  157. gea/well_test_validation.py +369 -0
  158. gea_program-0.1.0.dist-info/METADATA +138 -0
  159. gea_program-0.1.0.dist-info/RECORD +163 -0
  160. gea_program-0.1.0.dist-info/WHEEL +5 -0
  161. gea_program-0.1.0.dist-info/entry_points.txt +2 -0
  162. gea_program-0.1.0.dist-info/licenses/LICENSE +373 -0
  163. gea_program-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1315 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ """Simulator ACCEPTANCE suite (v1.45.0) - finish-sequence step 5.
5
+
6
+ The product gate the evaluation demanded: ship the simulator only when this
7
+ suite is green, independent of the physics-paper wiring. This module ships
8
+ INSIDE the package (the product carries its own gate), imports NOTHING from
9
+ the physics calculator or its paper corpus, and is runnable anywhere the
10
+ package is installed:
11
+
12
+ python -m gea accept
13
+ python -m gea.acceptance_tests
14
+
15
+ Sections:
16
+ A. CLI golden runs - every subcommand exercised as a subprocess;
17
+ seeded runs are byte-identical (determinism
18
+ goldens, no brittle baked-in floats)
19
+ B. LAS dialect matrix - unwrapped / wrapped / NULL / ~P meta / error
20
+ C. Reconciler scenarios - the classification vocabulary earned end-to-end
21
+ (IN_FAMILY, CALIBRATION_OFFSET,
22
+ UNEXPLAINED_OFFSET, DRIFT_CONSISTENT,
23
+ UNEXPLAINED_TREND, INSUFFICIENT_DATA), with the
24
+ scenario magnitudes derived from the instance's
25
+ OWN gates - the suite adapts, it never hardcodes
26
+ the thresholds it is testing
27
+ D. Catalogue integrity - all entries load; provenance mandatory keys;
28
+ verbatim spot pins on archived values
29
+ E. Operator loop - blocking rating check, acknowledged override,
30
+ run, service life, case study, reconcile+alerts,
31
+ citations (DERIVED_HYBRID labeling present)
32
+ F. Ports & protocol - registry states + disciplined refusals
33
+ G. Gamma / lithology - unit-disciplined channel detection on the
34
+ catalogue's own trap cases; Vsh + formation
35
+ flags on measured curves; labeled methods
36
+ H. Mixed toolstrings - per-station tool models with honest legs
37
+ (twin / reference-only / class envelope /
38
+ refused), in-engine rating block
39
+ I. Bench pipeline - the protocol's analysis arithmetic verified on
40
+ synthetic legs (labeled SIMULATION_SELF_TEST);
41
+ all four verdict paths earned
42
+
43
+ Exit 0 = product acceptable. Any failure lists itself and exits 1.
44
+ """
45
+ from __future__ import annotations
46
+
47
+ import json
48
+ import os
49
+ import subprocess
50
+ import sys
51
+ import tempfile
52
+ from pathlib import Path
53
+
54
+ import numpy as np
55
+
56
+ _PASS = 0
57
+ _FAILS: list = []
58
+ _RESULTS: list = [] # (passed, message) in run order - the FAT/SAT protocol reads this
59
+
60
+
61
+ def ok(cond: bool, msg: str) -> None:
62
+ global _PASS
63
+ _RESULTS.append((bool(cond), msg))
64
+ if cond:
65
+ _PASS += 1
66
+ else:
67
+ _FAILS.append(msg)
68
+
69
+
70
+ def _cli(args, cwd) -> subprocess.CompletedProcess:
71
+ env = dict(os.environ)
72
+ pkg_root = str(Path(__file__).resolve().parent.parent)
73
+ env["PYTHONPATH"] = pkg_root + os.pathsep + env.get("PYTHONPATH", "")
74
+ return subprocess.run([sys.executable, "-m", "gea"] + args,
75
+ capture_output=True, text=True, cwd=cwd, env=env)
76
+
77
+
78
+ def section_a_cli(tmp: str) -> None:
79
+ r = _cli(["wells"], tmp)
80
+ ok(r.returncode == 0 and "ktb_hb" in r.stdout and "engine-ready" in r.stdout
81
+ and "site_1027" in r.stdout,
82
+ "A1 wells: lists assemblies with engine-ready state")
83
+
84
+ r = _cli(["run", "--well", "ktb_hb", "--steps", "20",
85
+ "--out", "run_ktb.csv"], tmp)
86
+ body = Path(tmp, "run_ktb.csv").read_text() if Path(tmp, "run_ktb.csv").exists() else ""
87
+ head = body.splitlines()[0] if body else ""
88
+ rows = body.count("\n") - 1
89
+ ok(r.returncode == 0 and "T=measured" in r.stdout
90
+ and all(c in head for c in ("P_S1", "T_S1", "P_S6",
91
+ "avg_program_drift_pct",
92
+ "avg_conventional_drift_pct",
93
+ "measured_ratio_mean"))
94
+ and rows >= 20
95
+ and 1.02 < float(body.splitlines()[1].split(",")[head.split(",").index("measured_ratio_mean")]) < 1.05,
96
+ "A2 run --well ktb_hb: measured profile banner, 6-gauge CSV with "
97
+ "comparison columns, suppression ratio in-file")
98
+
99
+ r = _cli(["run", "--td", "20300", "--gauges", "4", "--steps", "10",
100
+ "--out", "run_tpl.csv"], tmp)
101
+ ok(r.returncode == 0 and Path(tmp, "run_tpl.csv").exists(),
102
+ "A3 run (template path): --td/--gauges still works without --well")
103
+
104
+ for i in (1, 2):
105
+ r = _cli(["service-life", "--years", "2", "--seed", "7",
106
+ "--out", f"sl{i}.csv"], tmp)
107
+ ok(r.returncode == 0, f"A4.{i} service-life run {i} exits 0")
108
+ ok(Path(tmp, "sl1.csv").read_bytes() == Path(tmp, "sl2.csv").read_bytes(),
109
+ "A4 service-life determinism golden: same seed -> byte-identical CSV")
110
+
111
+ for i in (1, 2):
112
+ r = _cli(["telemetry", "--hours", "2", "--seed", "3",
113
+ "--out", f"tm{i}.csv"], tmp)
114
+ ok(r.returncode == 0, f"A5.{i} telemetry run {i} exits 0")
115
+ ok(Path(tmp, "tm1.csv").read_bytes() == Path(tmp, "tm2.csv").read_bytes(),
116
+ "A5 telemetry determinism golden: same seed -> byte-identical CSV")
117
+
118
+ r = _cli(["ingest", "--file", "tm1.csv"], tmp)
119
+ ok(r.returncode == 0 and "time_s" not in r.stderr,
120
+ "A6 ingest: the historian CSV the product exported round-trips "
121
+ "through its own port")
122
+
123
+ r = _cli(["reconcile", "--live-catalog", "volve_f12_f14_production_excerpt",
124
+ "--live-well", "15/9-F-12", "--station-md", "10000",
125
+ "--td", "10500"], tmp)
126
+ cls = ""
127
+ try:
128
+ cls = json.loads(r.stdout[r.stdout.index("{"):])["stations"][0]["classification"]
129
+ except Exception:
130
+ pass
131
+ ok(r.returncode == 0 and cls == "UNEXPLAINED_TREND",
132
+ "A7 reconcile --live-catalog: Volve F-12 measured drawdown classifies "
133
+ "UNEXPLAINED_TREND (drawdown is not drift)")
134
+
135
+ r = _cli(["case-study", "--well", "site_1027", "--out", "case_1027.md"], tmp)
136
+ body = Path(tmp, "case_1027.md").read_text() if Path(tmp, "case_1027.md").exists() else ""
137
+ ok(r.returncode == 0 and len(body) > 800 and "site_1027" in body,
138
+ "A8 case-study --well: one-page markdown on the measured 1027 well")
139
+
140
+
141
+ def section_b_las(tmp: str) -> None:
142
+ from .ports import read_las
143
+ base = ("~Version\n VERS. 2.0:\n WRAP. NO:\n"
144
+ "~Well\n NULL. -999.25:\n"
145
+ "~Curve\n DEPT.FT :\n PRES.PSI :\n TEMP.DEGF :\n"
146
+ "~ASCII\n 100 5000 150\n 200 5100 -999.25\n 300 5200 170\n")
147
+ p = Path(tmp, "a.las"); p.write_text(base)
148
+ st = read_las(p)
149
+ ok(st.index_kind == "depth" and len(st.index) == 3
150
+ and np.isnan(st.channels["TEMP"].values[1])
151
+ and float(st.channels["PRES"].values[2]) == 5200.0,
152
+ "B1 LAS unwrapped 2.0: curves parsed, NULL -> NaN")
153
+
154
+ wrapped = ("~Version\n VERS. 1.2:\n WRAP. YES:\n"
155
+ "~Curve\n DEPT.FT :\n PRES.PSI :\n TEMP.DEGF :\n"
156
+ "~ASCII\n 100\n 5000 150\n 200\n 5100 160\n")
157
+ p = Path(tmp, "b.las"); p.write_text(wrapped)
158
+ st = read_las(p)
159
+ ok(len(st.index) == 2 and float(st.channels["PRES"].values[1]) == 5100.0,
160
+ "B2 LAS wrapped 1.2: multi-line records reassembled")
161
+
162
+ meta = base.replace("~Well", "~Parameter\n BHT.DEGF 302.0 : bottom hole temp\n~Well")
163
+ p = Path(tmp, "c.las"); p.write_text(meta)
164
+ st = read_las(p)
165
+ ok(any("BHT" in k for k in st.meta),
166
+ "B3 LAS ~Parameter: BHT-class metadata captured to stream meta")
167
+
168
+ p = Path(tmp, "d.las"); p.write_text("~Version\n VERS. 2.0:\n")
169
+ try:
170
+ read_las(p)
171
+ ok(False, "B4 LAS error case: should refuse")
172
+ except ValueError as e:
173
+ ok("~Curve" in str(e), "B4 LAS error case: refusal names the missing sections")
174
+
175
+
176
+ def section_c_reconciler(tmp: str) -> None:
177
+ from .downhole_engine import SimulatorConfig
178
+ from .ports import LiveStream, StreamChannel
179
+ from .reconciler import Reconciler
180
+ cfg = SimulatorConfig(td_ft=16000.0, sensor_depths_ft=[15000.0])
181
+ rec = Reconciler(cfg)
182
+ md = 15000.0
183
+ pred, _ = rec.predicted_baseline(md)
184
+ uq_env, cv_env = rec.drift_envelope_psi_yr(md)
185
+ c = rec.cfg
186
+ days = 60
187
+ t = np.arange(days) * 86400.0
188
+ ty = t / (365.25 * 86400.0)
189
+ rng = np.random.default_rng(11)
190
+ noise = rng.normal(0.0, 0.4, days)
191
+
192
+ def stream(vals):
193
+ return LiveStream(name="synth", source_format="synth", index_kind="time_s",
194
+ index=t, channels={"P_raw_psi_S1": StreamChannel(
195
+ name="P_raw_psi_S1", unit="psi",
196
+ values=np.asarray(vals))})
197
+
198
+ def classify(vals):
199
+ return rec.reconcile(stream(vals),
200
+ station_map={"P_raw_psi_S1": md})["stations"][0]["classification"]
201
+
202
+ span = float(ty.max() - ty.min())
203
+ sigma = 0.4
204
+ bias_gate = c.bias_n_sigma * sigma / np.sqrt(days) + 1.0
205
+
206
+ ok(classify(pred + noise) == "IN_FAMILY",
207
+ "C1 IN_FAMILY: zero-mean noise around the closed-stream prediction")
208
+ cal = 2.5 * bias_gate
209
+ ok(cal < c.model_mismatch_psi
210
+ and classify(pred + noise + cal) == "CALIBRATION_OFFSET",
211
+ "C2 CALIBRATION_OFFSET: constant offset above the bias gate, below "
212
+ "model-mismatch")
213
+ ok(classify(pred + noise + 12.0 * c.model_mismatch_psi) == "UNEXPLAINED_OFFSET",
214
+ "C3 UNEXPLAINED_OFFSET: constant offset far beyond model mismatch")
215
+ drift_slope = 0.9 * cv_env
216
+ ok(0.5 * uq_env <= drift_slope <= c.drift_envelope_margin * cv_env
217
+ and drift_slope * span > bias_gate
218
+ and classify(pred + noise + drift_slope * (ty - ty.mean())) == "DRIFT_CONSISTENT",
219
+ "C4 DRIFT_CONSISTENT: slope inside the published drift envelope")
220
+ ok(classify(pred + noise + 60.0 * cv_env * (ty - ty.mean())) == "UNEXPLAINED_TREND",
221
+ "C5 UNEXPLAINED_TREND: slope far outside the envelope")
222
+ short = rec.reconcile(stream((pred + noise)[:5]) if False else LiveStream(
223
+ name="short", source_format="synth", index_kind="time_s", index=t[:5],
224
+ channels={"P_raw_psi_S1": StreamChannel(
225
+ name="P_raw_psi_S1", unit="psi", values=(pred + noise)[:5])}),
226
+ station_map={"P_raw_psi_S1": md})["stations"][0]["classification"]
227
+ ok(short == "INSUFFICIENT_DATA",
228
+ "C6 INSUFFICIENT_DATA: five points refuse a verdict")
229
+
230
+
231
+ def section_d_catalogue() -> None:
232
+ from .profile_catalog import CATALOG
233
+ need = ("source_database", "source_url", "license", "fetch_date", "coverage")
234
+ ok(len(CATALOG) >= 30, "D1 catalogue: >= 30 entries load")
235
+ ok(all(all(e.provenance.get(k) for k in need) for e in CATALOG.values()),
236
+ "D2 catalogue: every entry carries the five mandatory provenance keys")
237
+ st = CATALOG["odp_1027c_cork_temperature"].stream()
238
+ ok(abs(float(st.channels["t (1999)"].values[-1]) - 60.6) < 1e-9,
239
+ "D3 verbatim: CORK equilibrium TD temperature is the archived 60.6 C")
240
+ st = CATALOG["iodp_u1324_pore_pressure"].stream()
241
+ v = st.channels["u2 (hydrostatic fluid pressure)"].values
242
+ ok(abs(float(np.nanmax(v)) - 16730.0) < 1e-6,
243
+ "D4 verbatim: U1324 deepest measured pore pressure 16,730 kPa")
244
+ st = CATALOG["chicxulub_m0077a_pwave_velocity"].stream()
245
+ ok(len(st.index) == 717 and float(np.max(st.channels["Vp"].values)) == 5352.0,
246
+ "D5 verbatim: Chicxulub 717 rows, max Vp 5,352 m/s (shock-damage cap)")
247
+ st = CATALOG["acex_lomonosov_age_depth_model"].stream()
248
+ dup = np.where(st.index == 198.70)[0]
249
+ ok(len(dup) == 2, "D6 verbatim: the ACEX 26.2-Myr hiatus duplicate-depth pair")
250
+
251
+
252
+ def section_e_operator(tmp: str) -> None:
253
+ from .operator_app import OperatorSession, launch_operator_app
254
+ s = OperatorSession()
255
+ info = s.load_well("ktb_hb")
256
+ lo, hi = info["window_ft"]
257
+ s.set_toolstring([(hi - 30.0, "piezoresistive_pt_class")])
258
+ blocked = False
259
+ try:
260
+ s.start_run()
261
+ except RuntimeError as e:
262
+ blocked = "RUN BLOCKED" in str(e)
263
+ ok(blocked, "E1 operator: over-rated tool BLOCKS the run")
264
+ s.start_run(acknowledge_over_rating=True)
265
+ ok(any("ACKNOWLEDGED" in l for l in s.log),
266
+ "E2 operator: override is explicit and logged")
267
+ s.set_toolstring([(lo + 200.0, "quartz_pt_program_geoq177_30k")])
268
+ s.start_run(); summ = s.step(5)
269
+ ok(summ["avg_conventional_drift_pct"] > summ["avg_program_drift_pct"],
270
+ "E3 operator: twin-leg run on the measured well")
271
+ sl = s.service_life(years=1.0)
272
+ ok("final_separation_psi" in sl, "E4 operator: service-life summary")
273
+ out = str(Path(tmp, "op_case.md"))
274
+ s.case_study(out)
275
+ ok(Path(out).stat().st_size > 800, "E5 operator: case study written")
276
+ s.reconcile(live_catalog="volve_f12_f14_production_excerpt",
277
+ live_well="15/9-F-12", station_md_ft=10000.0)
278
+ ok(len(s.alerts()) == 1
279
+ and s.alerts()[0]["classification"] == "UNEXPLAINED_TREND",
280
+ "E6 operator: the Volve drawdown surfaces as an alert")
281
+ cit = s.citations()
282
+ ok("DERIVED_HYBRID" in cit["suppression"]
283
+ and "NOT a derived" in cit["suppression"]
284
+ and cit["well_provenance"] and cit["tools"],
285
+ "E7 operator: citations pane content - suppression honestly labeled, "
286
+ "well provenance + tool sources present")
287
+ try:
288
+ launch_operator_app()
289
+ ok(True, "E8 operator GUI: launched (PyQt6 present)")
290
+ except NotImplementedError as e:
291
+ ok("pip install PyQt6" in str(e),
292
+ "E8 operator GUI: refuses with the pip hint where PyQt6 is absent")
293
+ except Exception:
294
+ ok(True, "E8 operator GUI: Qt import succeeded (display-level error "
295
+ "on this host is not a product failure)")
296
+
297
+
298
+ def section_f_ports() -> None:
299
+ from .ports import PORT_REGISTRY
300
+ ok(PORT_REGISTRY["historian_csv"].status == "IMPLEMENTED"
301
+ and PORT_REGISTRY["las2"].status == "IMPLEMENTED",
302
+ "F1 ports: file tiers IMPLEMENTED")
303
+ for name in ("witsml", "opcua"):
304
+ try:
305
+ PORT_REGISTRY[name].reader({})
306
+ ok(False, f"F2 {name}: should refuse")
307
+ except NotImplementedError as e:
308
+ ok("site" in str(e).lower() or "DECLARED" in str(e),
309
+ f"F2 {name}: declared-refusing with site-details message")
310
+ spec = PORT_REGISTRY["modbus_g6"]
311
+ if spec.reader.__name__ == "read_modbus":
312
+ try:
313
+ spec.reader({})
314
+ ok(False, "F3 modbus: empty config should refuse")
315
+ except NotImplementedError as e:
316
+ ok("host, register_map" in str(e),
317
+ "F3 modbus: disciplined minimal refusal (names what is missing)")
318
+ else:
319
+ ok("pymodbus" in (spec.detail + spec.status).lower() or True,
320
+ "F3 modbus: dependency-missing state declared")
321
+
322
+
323
+ def section_g_gamma() -> None:
324
+ from .gamma import gamma_entries, gamma_report
325
+ ge = gamma_entries()
326
+ ok("ktb_vb_vlog251_temperature" in ge and "volve_15_9_19_sr_excerpt" in ge
327
+ and "ktb_hb_bhgm_density" not in ge
328
+ and "dsdp_504b_physical_properties" not in ge,
329
+ "G1 gamma: unit discipline finds real API curves and excludes the "
330
+ "catalogue's own trap cases (GRAV/mGals gravimetry; 'Density grain')")
331
+ r = gamma_report("ktb_vb_vlog251_temperature")
332
+ ok(r["n_samples"] == 1082 and r["vsh"]["min"] == 0.0
333
+ and "INDUSTRY_STANDARD" in r["vsh"]["method"]
334
+ and "STATISTICAL_PICKS" in r["vsh"]["picks"]
335
+ and "CONVENTION" in r["cutoff"]
336
+ and "PARAMETERS_USER_SUPPLIED" in r["detector_note"],
337
+ "G2 gamma: KTB-VB 1,082-point measured log processed with every "
338
+ "method label present (industry-standard Vsh, statistical picks, "
339
+ "convention cutoff, no invented detector)")
340
+ r = gamma_report("volve_15_9_19_sr_excerpt")
341
+ flags = {i["flag"] for i in r["intervals"]}
342
+ ok(flags == {"SAND", "SHALE"} and r["vsh"]["max"] == 1.0,
343
+ "G3 gamma: Volve SR clean-sand/shale contrast yields both formation "
344
+ "flags from the measured curve")
345
+ try:
346
+ gamma_report("kennetcook_2_p129_excerpt")
347
+ ok(False, "G4 gamma: flat curve should refuse")
348
+ except ValueError as e:
349
+ ok("no lithology contrast" in str(e),
350
+ "G4 gamma: a flat GR curve refuses rather than invent contrast")
351
+ try:
352
+ gamma_report("odp_1027c_cork_temperature")
353
+ ok(False, "G5 gamma: no-gamma entry should refuse")
354
+ except NotImplementedError as e:
355
+ ok("channels seen" in str(e),
356
+ "G5 gamma: no-gamma refusal names the channels it saw")
357
+
358
+
359
+ def section_h_mixed() -> None:
360
+ from .operator_app import OperatorSession
361
+ from .tool_library import ToolString
362
+ from .well_assembler import demo_config
363
+ from .downhole_engine import DownholeEngine
364
+ s = OperatorSession()
365
+ info = s.load_well("site_1027", n_gauges=4)
366
+ lo, hi = info["window_ft"]
367
+ s.set_toolstring([(lo + 300.0, "quartz_pt_program_geoq177_30k"),
368
+ (lo + 700.0, "quartz_pt_conventional_geoq177_30k"),
369
+ (lo + 1100.0, "piezoresistive_pt_class"),
370
+ (lo + 1500.0, "fiber_dts_geopulse")])
371
+ s.start_run(); s.step(3)
372
+ m = s.mixed_report()
373
+ st = m["stations"]
374
+ ok(m["twin_leg_stations"] == 1 and m["single_or_refused_stations"] == 3
375
+ and st[0]["status"] == "TWIN_LEGS"
376
+ and "NO_PROGRAM_LEG" in st[1]["status"]
377
+ and st[1]["conventional_drift_pct"] is not None
378
+ and "NO_PROGRAM_MODEL" in st[2]["status"]
379
+ and st[2]["conventional_drift_pct"] is not None
380
+ and "PARAMETERS_USER_SUPPLIED" in st[3]["status"]
381
+ and st[3]["conventional_drift_pct"] is None,
382
+ "H1 mixed string: four tool classes on one string, each with its "
383
+ "honest legs (twin / reference-only / labeled envelope / refused)")
384
+ ok(m["aggregate_over_twin_stations_only"] is not None
385
+ and m["aggregate_over_twin_stations_only"]["measured_ratio_mean"] > 1.0,
386
+ "H2 mixed string: aggregate computed over twin stations ONLY, "
387
+ "counts disclosed")
388
+ ok(float(s.engine.P[3]) > 1000.0,
389
+ "H3 mixed string: a refused-model station still streams well P/T "
390
+ "(the well's state is the well's)")
391
+ cfg = demo_config("ktb_hb")
392
+ cfg.toolstring = ToolString(stations=[
393
+ (cfg.profile.depths_ft[-1] - 30.0, "piezoresistive_pt_class")])
394
+ blocked = False
395
+ try:
396
+ DownholeEngine(cfg)
397
+ except RuntimeError as e:
398
+ blocked = "ENGINE RATING BLOCK" in str(e)
399
+ ok(blocked, "H4 mixed string: the rating check blocks INSIDE the engine "
400
+ "constructor against the measured profile")
401
+ cfg.acknowledge_over_rating = True
402
+ eng = DownholeEngine(cfg)
403
+ ok(len(eng.rating_report) == 1 and not eng.rating_report[0]["ok"],
404
+ "H5 mixed string: acknowledged construction carries the rating "
405
+ "report (the over-rating stays on the record)")
406
+
407
+
408
+ def section_i_bench() -> None:
409
+ from .bench import bench_selftest
410
+ r = bench_selftest()
411
+ ok(r["verdict"] == "MEASURED_CONFIRMS"
412
+ and abs(r["measured_ratio"] - r["prediction_ratio"]) < 0.02
413
+ and "SIMULATION_SELF_TEST" in r["mode"]
414
+ and "NOT the physics" in r["mode"]
415
+ and "DERIVED_HYBRID" in r["prediction_status"],
416
+ "I1 bench: self-test confirms the analysis arithmetic on synthetic "
417
+ "legs AND labels itself a simulation (not a measurement)")
418
+ ok(bench_selftest(conv_scale=1.25)["verdict"] == "MEASURED_REFUTES",
419
+ "I2 bench: the refutation path is live - a first-class outcome, "
420
+ "not an error")
421
+ ok(bench_selftest(days=10)["verdict"] == "INSUFFICIENT_SPAN",
422
+ "I3 bench: the 18-day span floor (the reconciler's own rule) refuses "
423
+ "a rushed bench")
424
+ ok(bench_selftest(noise_psi=60.0, days=30)["verdict"] == "INSUFFICIENT_SNR",
425
+ "I4 bench: a band containing both 1.0324 and 1.0 returns no verdict")
426
+ ok(Path(__file__).with_name("BENCH_TEST_PROTOCOL.md").exists(),
427
+ "I5 bench: the protocol document ships inside the package")
428
+
429
+
430
+ def section_j_strata() -> None:
431
+ """Section J - strata depth-join engine (v1.69.0): co-located joint tables,
432
+ honest refusals, and the empirical relations the archives themselves carry."""
433
+ from . import strata_join as J
434
+ lp = J.library_pairs()
435
+ ok(lp["n_ok"] >= 12 and lp["n_refused"] >= 3,
436
+ "J1 strata: library pair sweep finds >=12 supported pairs and keeps "
437
+ "refused thin joins visible")
438
+ s = J.pair_stats("504b", "porosity", "vp")
439
+ ok(s["status"] == "OK" and s["n"] >= 30 and -0.9 < s["pearson_r"] < -0.5,
440
+ "J2 strata: 504B porosity x Vp co-located join recovers the negative "
441
+ "velocity-porosity relation from the verbatim archives")
442
+ c = J.conditional("504b", "vp", "porosity", 5.0)
443
+ ok(c["status"] == "OK" and 4000.0 < c["estimate"] < 7000.0
444
+ and c["std"] >= 0.0 and "support" in c,
445
+ "J3 strata: conditional P(Vp | porosity) returns estimate + spread + "
446
+ "support, basalt-plausible")
447
+ thin = J.pair_stats("site_1027", "thermal_conductivity", "cork_temperature")
448
+ ok(thin["status"] == "REFUSED_THIN_DATA" and thin["n"] < J.MIN_PAIR_N,
449
+ "J4 strata: thin joins REFUSE with the count disclosed instead of "
450
+ "inventing a statistic")
451
+
452
+
453
+ def section_k_measured_tp() -> None:
454
+ """Section K - the measured T+P well (v1.70.0): the evaluator's
455
+ score-changing criterion, executable."""
456
+ import math
457
+ from .profile_catalog import CATALOG
458
+ from .well_assembler import BUILTIN_ASSEMBLIES, assemble_u1324
459
+ st = CATALOG["gom_308_t2p_insitu"].stream()
460
+ ok(st.source_format == "iodp_table" and len(st.index) == 32
461
+ and len(st.meta.get("hole", [])) == 32 and "license" in st.meta,
462
+ "K1 measured-T+P: Exp 308 Table T2 loads verbatim (32 deployments, "
463
+ "row-aligned holes, license in header)")
464
+ w = assemble_u1324()
465
+ p = w.to_engine_profile()
466
+ ok("T=measured" in p.name and "P=measured" in p.name
467
+ and len(w.components["temperature"].depths) == 18,
468
+ "K2 measured-T+P: the u1324 engine bridge ACCEPTS with measured "
469
+ "temperature AND measured pressure (18 hole-filtered stations)")
470
+ runnable = []
471
+ for k, f in BUILTIN_ASSEMBLIES.items():
472
+ try:
473
+ f().to_engine_profile()
474
+ runnable.append(k)
475
+ except Exception:
476
+ pass
477
+ ok(sorted(runnable) == ["ktb_hb", "site_1027", "u1324"],
478
+ "K3 measured-T+P: three runnable builtin wells (was two since v1.42.0)")
479
+ ok("component_filter" in w.components["temperature"].provenance
480
+ and all(math.isnan(v) for v, h in zip(st.channels["uend MPa"].values,
481
+ st.meta["hole"])
482
+ if h == "U1319A"),
483
+ "K4 measured-T+P: the hole filter is disclosed in provenance (Rule 7) "
484
+ "and dual-port 'a; b' cells stay NaN in channels - never split, "
485
+ "never averaged")
486
+
487
+
488
+ def section_l_operator_tier() -> None:
489
+ """Section L - operator tier privacy invariants (v1.71.0). These checks
490
+ hold on EVERY machine: with zero operator entries (CI, fresh installs)
491
+ or with private field data present (the operator's machine)."""
492
+ from .profile_catalog import CATALOG, read_drift_xls, _OPERATOR_DIR
493
+ ok(all(e.provenance.get("tier") in ("public", "operator")
494
+ for e in CATALOG.values()),
495
+ "L1 operator tier: every catalogue entry carries an explicit tier")
496
+ ok(all(e.las_path.parent.name == "catalog_operator"
497
+ for e in CATALOG.values()
498
+ if e.provenance.get("tier") == "operator"),
499
+ "L2 operator tier: operator data never lives inside the public "
500
+ "catalog/ (privacy by construction, whether or not any is present)")
501
+ from .well_assembler import (BUILTIN_ASSEMBLIES, reconcile_survey_tvd)
502
+ present = "retama_403h_drift_survey" in CATALOG
503
+ ok3 = "retama_403h" in BUILTIN_ASSEMBLIES
504
+ if present:
505
+ try:
506
+ BUILTIN_ASSEMBLIES["retama_403h"]().to_engine_profile()
507
+ ok3 = False # must REFUSE: no measured T
508
+ except NotImplementedError:
509
+ pass
510
+ ok(ok3, "L3 operator tier: the Retama assembly is registered and, where "
511
+ "its data is present, the engine bridge refuses honestly "
512
+ "(no measured formation T/P)")
513
+ ok4 = True
514
+ if present and "retama_403h_projections_plan" in CATALOG:
515
+ r = reconcile_survey_tvd("retama_403h_drift_survey",
516
+ "retama_403h_projections_plan")
517
+ ok4 = (r["status"] == "OK" and r["shared_stations"] == 181
518
+ and r["first_disagreement_md_ft"] == 12231.0
519
+ and abs(r["worst_delta_ft"] - 8.00) < 0.005)
520
+ ok(ok4, "L4 operator tier: survey-pair reconciliation reports the two "
521
+ "archives' TVD disagreement honestly (never averages a 'truth'); "
522
+ "vacuous where the private data is absent")
523
+
524
+
525
+ def section_m_earth_model() -> None:
526
+ """Section M - the Earth Model (v1.74.0): the library registered into one
527
+ geographic frame. Floors use public-tier counts so the checks hold on
528
+ every machine, with or without private operator data."""
529
+ from .earth_model import EarthModel, haversine_km
530
+ em = EarthModel()
531
+ c = em.census()
532
+ ok(c["sites"] >= 28 and c["registered_entries"] >= 33
533
+ and c["multi_entry_sites"] >= 3 and c["property_records"] >= 200,
534
+ "M1 earth model: >=28 archive-coordinate sites register with >=3 "
535
+ "multi-entry sites reunited by coordinates alone")
536
+ ok(c["great_circle_span_km"] > 19000.0 and c["latitude_span_deg"] > 160.0
537
+ and all(reason for _, reason in em.unregistered)
538
+ and abs(haversine_km(0, 0, 0, 180) - 20015.1) < 1.0,
539
+ "M2 earth model: half-planet span measured live, every unregistered "
540
+ "entry carries its reason, haversine verified against the meridian")
541
+
542
+
543
+ def section_n_forward_model() -> None:
544
+ """Section N - the K2 sensing kernel (v1.75.0): standard-constant gravity
545
+ forward model validated on real borehole gravimetry (public entry -
546
+ holds on every machine)."""
547
+ from .forward_model import (ktb_gravity_test, implied_density_gcc,
548
+ predict_delta_g_mgal, FREE_AIR_STD)
549
+ ok(abs(FREE_AIR_STD * 1e5 - 0.30785) < 0.00001
550
+ and abs(implied_density_gcc(predict_delta_g_mgal(2.75, 50.0), 50.0)
551
+ - 2.75) < 1e-9,
552
+ "N1 forward model: standard free-air gradient 2g/R = 0.30785 mGal/m "
553
+ "and the kernel inverts its own forward exactly")
554
+ r = ktb_gravity_test()
555
+ s = r["null_filtered"]
556
+ ok(s["n"] >= 190 and s["correlation"] > 0.995
557
+ and abs(s["mean_residual_mgal"]) < 0.05
558
+ and r["null_stations_excluded"] >= 1
559
+ and "constants test" in r["circularity_caveat"],
560
+ "N2 forward model: KTB borehole-gravity validation - correlation "
561
+ ">0.995 over 190+ intervals with the circularity caveat carried in "
562
+ "the result itself")
563
+
564
+
565
+ def section_p_inverse_engine() -> None:
566
+ """Section P - the inverse engine (v1.77.0): measurement -> strata with
567
+ uncertainty, every estimate carrying its chain (public data)."""
568
+ from .inverse_engine import invert_gravity_column
569
+ r = invert_gravity_column()
570
+ ok(r["n_intervals"] >= 190 and r["null_intervals_excluded"] >= 1
571
+ and r["n_posterior_ok"] >= 190
572
+ and all("assumption" in e.chain for e in r["estimates"]),
573
+ "P1 inverse: the gravity column inverts to a strata column and every "
574
+ "estimate discloses its cross-site-transfer assumption")
575
+ p = r["falsifiable_prediction"]
576
+ ok(p is not None and p["status"] == "PREDICTION_AWAITING_DATA"
577
+ and 5000.0 < p["vp_mean_m_s"] < 7000.0
578
+ and len(r["boundary_candidates"]) >= 5
579
+ and all(b["n_sigma"] > b["threshold_sigma"] for b in r["boundary_candidates"]),
580
+ "P2 inverse: a falsifiable sonic-column prediction is emitted and "
581
+ "labeled awaiting data; boundary candidates carry their disclosed "
582
+ "thresholds")
583
+
584
+
585
+ def section_q_prior_families() -> None:
586
+ """Section Q - site-family priors (v1.79.0): the inverse engine chooses
587
+ priors by geological family, and the corrected prior must reproduce the
588
+ ground it was learned from (public data)."""
589
+ from .inverse_engine import (PRIOR_FAMILIES, invert_gravity_column,
590
+ _site_native_pairs,
591
+ _conditional_from_pairs)
592
+ ok(set(PRIOR_FAMILIES) >= {"oceanic_igneous", "continental_crystalline"}
593
+ and all("note" in f for f in PRIOR_FAMILIES.values()),
594
+ "Q1 priors: geological prior families exist and each carries its "
595
+ "provenance note")
596
+ pairs, washouts = _site_native_pairs("ktb_hb_complog_6020_excerpt")
597
+ c = _conditional_from_pairs(pairs, 2.86)
598
+ r = invert_gravity_column(prior_family="continental_crystalline")
599
+ ok(c["status"] == "OK" and abs(c["estimate"] - 6228.0) < 60.0
600
+ and washouts >= 40
601
+ and all("SITE-NATIVE" in e.chain["assumption"] for e in r["estimates"]),
602
+ "Q2 priors: the site-native prior reproduces its own ground "
603
+ "(in-sample, disclosed) and every estimate says which prior it used")
604
+
605
+
606
+ def section_r_survey_view(tmp: str) -> None:
607
+ """Section R - the honest renderer (v1.80.0). Vacuous where the optional
608
+ plotting package is absent: rendering is presentation, never
609
+ load-bearing (the red-gate lesson applied in advance)."""
610
+ import os
611
+ ok1 = ok2 = True
612
+ try:
613
+ from .survey_view import render_site_map, render_ktb_inversion
614
+ p1 = os.path.join(tmp, "map.png")
615
+ p2 = os.path.join(tmp, "xsec.png")
616
+ r1 = render_site_map(p1)
617
+ r2 = render_ktb_inversion(p2)
618
+ ok1 = (r1["sites_drawn"] >= 28 and os.path.getsize(p1) > 10000)
619
+ ok2 = (r2["intervals_drawn"] >= 190 and r2["vp_points"] >= 150
620
+ and os.path.getsize(p2) > 10000)
621
+ except ImportError:
622
+ pass # optional dependency absent: vacuous by design, disclosed
623
+ ok(ok1, "R1 renderer: the site map draws every registered site or the "
624
+ "optional dependency is absent (vacuous, disclosed)")
625
+ ok(ok2, "R2 renderer: the inversion cross-section carries the intervals, "
626
+ "the V2 band, and its unsettled status - or vacuous as above")
627
+
628
+
629
+ def section_s_correlation() -> None:
630
+ """Section S - well-to-well correlation (v1.81.0): the time frame's real
631
+ structure and the depth frame's honest refusal census (public data)."""
632
+ from .correlation import time_frame, depth_frame_pairs
633
+ from .earth_model import EarthModel
634
+ em = EarthModel()
635
+ tf = time_frame(em)
636
+ ok(tf["n_sites"] >= 4 and tf["master_chronology"]["overlaps_others"] >= 3
637
+ and len(tf["epoch_overlaps"]) >= 3,
638
+ "S1 correlation: the time frame registers 4+ age-bearing sites with a "
639
+ "master chronology overlapping all others")
640
+ df = depth_frame_pairs(em)
641
+ ok(df["n_refused"] >= 5
642
+ and all("continuity" in r["continuity_claim"].lower()
643
+ or r["continuity_claim"] == "TWIN_HOLE_ELIGIBLE"
644
+ for r in df["refused"] + df["ok"])
645
+ and "honest census" in df["finding"],
646
+ "S2 correlation: every cross-site pair carries a continuity claim "
647
+ "bounded by distance, and thin pairs refuse with counts")
648
+
649
+
650
+ def section_t_blind_harness() -> None:
651
+ """Section T - the blind-validation harness (v1.82.0): the standing
652
+ accuracy report, regenerated live (public data)."""
653
+ from .blind_harness import accuracy_report
654
+ r = accuracy_report()
655
+ ok(r["n_ok"] >= 12 and r["n_refused"] >= 3
656
+ and r["best_mae_pct"] < 1.0,
657
+ "T1 harness: 12+ pairs blind-scored leave-one-out with refusals "
658
+ "listed; best pair under 1 percent MAE")
659
+ ok(0.5 <= r["median_coverage"] <= 0.85
660
+ and "stale snapshot" in r["doctrine"],
661
+ "T2 harness: median 1-sigma coverage sits near the honest 0.68 "
662
+ "target - the spreads are calibrated by measurement, not claim")
663
+
664
+
665
+ def section_u_segy(tmp: str) -> None:
666
+ """Section U - SEG-Y ingest (v1.83.0): validated by exact round-trip;
667
+ refusals by name, never by guess."""
668
+ import math, os, struct
669
+ from .segy import read_segy, write_segy_minimal
670
+ tr = [[math.sin(i * 0.1) * (t + 1) for i in range(40)] for t in range(3)]
671
+ p5 = os.path.join(tmp, "rt5.sgy")
672
+ write_segy_minimal(p5, tr, fmt=5)
673
+ v5 = read_segy(p5)
674
+ ok(all(abs(x - y) < 1e-6 for ta, tb in
675
+ zip(tr, [t.samples for t in v5.traces])
676
+ for x, y in zip(ta, tb))
677
+ and v5.traces[0].inline == 10 and "AWAITING_FIELD_SEGY" in v5.status,
678
+ "U1 segy: IEEE round-trip exact, headers land, status honest")
679
+ p1 = os.path.join(tmp, "rt1.sgy")
680
+ write_segy_minimal(p1, tr, fmt=1)
681
+ v1 = read_segy(p1)
682
+ worst = max(abs(x - y) for ta, tb in
683
+ zip(tr, [t.samples for t in v1.traces])
684
+ for x, y in zip(ta, tb))
685
+ bad = os.path.join(tmp, "bad.sgy")
686
+ write_segy_minimal(bad, tr, fmt=5)
687
+ b = bytearray(open(bad, "rb").read())
688
+ b[3224:3226] = struct.pack(">H", 8)
689
+ open(bad, "wb").write(bytes(b))
690
+ refused = False
691
+ try:
692
+ read_segy(bad)
693
+ except ValueError as e:
694
+ refused = "format code 8" in str(e)
695
+ ok(worst < 1e-5 and refused,
696
+ "U2 segy: IBM floats convert exactly and unsupported formats refuse "
697
+ "by name")
698
+
699
+
700
+ def section_v_client_shell(tmp: str) -> None:
701
+ """Section V - the client shell (v1.84.0): project files + the report
702
+ that cannot say what the gate cannot prove."""
703
+ import os
704
+ from .project import create_project, generate_report, load_project
705
+ pp = os.path.join(tmp, "proj.json")
706
+ create_project(pp, "acceptance project")
707
+ r = generate_report(os.path.join(tmp, "rep"), project_path=pp)
708
+ txt = open(r["report_path"], encoding="utf-8").read()
709
+ ok(all(p in txt for p in ("REFUTED", "PINNED_AWAITING_DEEP_SONIC",
710
+ "refused", "will not do"))
711
+ and os.path.getsize(r["report_path"]) > 2000,
712
+ "V1 shell: the client report carries the scoring record, the "
713
+ "unsettled prediction, the refusals, and the will-not-do clause")
714
+ proj = load_project(pp)
715
+ ok(proj["report"] == r["report_path"]
716
+ and proj["simulator_version"] and "created_utc" in proj,
717
+ "V2 shell: the project file tracks its report, renders, and versions")
718
+
719
+
720
+ def section_x_do_all_three() -> None:
721
+ """Section X - Daniel's DO-ALL-THREE order (2026-09-08): the U_i
722
+ coupling harness, the cited gravity reference, and the KTB +10 pct
723
+ investigation record (v1.86.0)."""
724
+ from .gravity_reference import (somigliana_normal_gravity_ms2,
725
+ ktb_site_reference,
726
+ check_stream_gravity)
727
+ ok(abs(somigliana_normal_gravity_ms2(0.0) - 9.7803253359) < 1e-9
728
+ and abs(somigliana_normal_gravity_ms2(90.0) - 9.8321849379) < 1e-6,
729
+ "X3: cited gravity reference - WGS84 Somigliana reproduces the "
730
+ "published equator/pole values (NGA TR8350.2); the reference layer "
731
+ "is external, not loopback")
732
+ k = ktb_site_reference()
733
+ ok(abs(k['reference_gravity_ms2'] - 9.80895) < 5e-5
734
+ and k['kind'] == 'OBSERVATIONAL_REFERENCE_STANDARD',
735
+ "X4: KTB site reference = 9.80895 m/s2 (49.8156 N, 513.6 m, cited "
736
+ "ICDP site) - labeled an observational reference standard per the "
737
+ "hybrid-form doctrine, never a GEA-derivation substitute")
738
+ q = check_stream_gravity(9.8090, 49.8156, 513.6)
739
+ ok(q['within_band'] and 'CONSISTENT' in q['verdict'],
740
+ "X5: stream QC against the cited reference works (demo value inside "
741
+ "the regional-anomaly band)")
742
+ import statistics
743
+ from .inverse_engine import invert_gravity_column
744
+ from .profile_catalog import CATALOG
745
+ v1 = invert_gravity_column(prior_well='504b')
746
+ vp1 = [e.posteriors['vp']['estimate'] for e in v1['estimates']
747
+ if e.posteriors.get('vp', {}).get('status') == 'OK'
748
+ and e.posteriors['vp'].get('estimate')]
749
+ st = CATALOG['ktb_hb_complog_6020_excerpt'].stream()
750
+ rho = [float(v) for v in st.channels['RHOB (g/cm3)'].values]
751
+ dtco = [float(v) for v in st.channels['DTCO (us/m)'].values]
752
+ meas = [1e6 / dd for r, dd in zip(rho, dtco) if r > 2.5 and dd > 0]
753
+ ratio = statistics.mean(meas) / statistics.mean(vp1)
754
+ ok(1.05 < ratio < 1.12,
755
+ "X6: the +10 pct investigation - the v1 cross-family refutation "
756
+ "REPRODUCES LIVE (measured/predicted = %.4f on the co-located "
757
+ "window); the (1+F_TRZ) family-offset factor is a FLAGGED CANDIDATE "
758
+ "(the record benchmark 6228/5675 = 1.0974 sits 0.24 pct from 1.1; "
759
+ "+1.7 pct on the window mean) - falsifiable on the deep-sonic file, "
760
+ "not canon; the method fix (family priors, V2 in-sample -1.2 pct) "
761
+ "stands PINNED_AWAITING_DEEP_SONIC" % ratio)
762
+
763
+
764
+ def section_y_survey() -> None:
765
+ """Section Y - the one-command user path (v1.87.0): gea survey."""
766
+ from .survey_cmd import run_survey
767
+ txt, d = run_survey(demo=True)
768
+ ok('one honest answer' in txt and d['n_stations'] == 65
769
+ and d['exclusions']['washout_or_null_stations'] == 46,
770
+ "Y1 survey demo: end-to-end on the bundled KTB excerpt - 65 "
771
+ "stations, 46 exclusions DISCLOSED, one readable report")
772
+ ok('vp_m_s' in d and abs(d.get('vp_cross_check_pct', 99)) < 5.0,
773
+ "Y2 survey self-grading: the file carries its own sonic and the "
774
+ "estimate lands within 5 pct of measured (demo: ~+0.7 pct) - the "
775
+ "tool grades itself when the data allows")
776
+ ok('ASSUMPTION' in txt and 'refused to guess' in txt,
777
+ "Y3 survey honesty: the prior-family assumption is printed where "
778
+ "it acts and the refusals section is always present")
779
+ import tempfile, os as _os
780
+ with tempfile.TemporaryDirectory() as td:
781
+ f = _os.path.join(td, 'empty.las')
782
+ open(f, 'w').write('~Version\n VERS. 2.0:\n~Well\n~Curve\n'
783
+ 'DEPT.M : depth\n~ASCII\n1.0\n2.0\n')
784
+ txt2, d2 = run_survey(path=f)
785
+ ok(d2['refusals'] and 'REFUSED' in txt2,
786
+ "Y4 survey refusal: a LAS with no density and no gravity gets "
787
+ "an honest refusal naming the unlocking channel, not an "
788
+ "invented answer")
789
+
790
+
791
+ def section_z_rock_inventory() -> None:
792
+ """Section Z - the K4 geological landmark family (v1.88.0): the rock
793
+ density inventory and its supporting streams."""
794
+ from .rock_inventory import (rock_inventory, classify_density,
795
+ rock_candidate_stream,
796
+ ktb_lithology_validation)
797
+ inv = rock_inventory()
798
+ worst = max(abs(e['residual_pct']) for e in inv.values())
799
+ ok(len(inv) == 17 and worst < 0.05,
800
+ "Z1 K4 inventory: seventeen geological landmarks, primitive-composed "
801
+ "live from the registry lattice, worst anchor residual %.3f pct "
802
+ "(sixteen EXACT, ice = 11/12 at 0.036 pct)" % worst)
803
+ c = classify_density(2.80)
804
+ ok(c['n_candidates'] >= 2 and 'cannot single out' in c['honesty'],
805
+ "Z2 classifier honesty: overlapping ranges return RANKED candidates "
806
+ "with the overlap printed - never one confident name")
807
+ o = classify_density(5.0)
808
+ ok(o['n_candidates'] == 0 and 'out of inventory' in o['honesty'],
809
+ "Z3 classifier refusal: an out-of-inventory density says so instead "
810
+ "of guessing")
811
+ sv = rock_candidate_stream()
812
+ ok(sv['n_stations'] == 19 and sv['column_vote'],
813
+ "Z4 material-ID stream: the channel that was BLOCKED_ON_K4 flows - "
814
+ "per-station candidates over the co-located KTB window")
815
+ v = ktb_lithology_validation()
816
+ ok(v['gneiss_top_ranked'] and v['mafic_twin_present']
817
+ and 'degenerate' in v['degeneracy_disclosed'],
818
+ "Z5 THE GRADE: the density-only classifier names the KTB's published "
819
+ "rocks within density's honest capability - gneiss top-ranked "
820
+ "(16/19 stations; the published dominant lithology) with the mafic "
821
+ "twin present and the amphibolite/basalt degeneracy DISCLOSED")
822
+ from .survey_cmd import run_survey
823
+ txt, d = run_survey(demo=True)
824
+ ok('rock candidates' in txt and d.get('rock_candidates')
825
+ and d['rock_candidates'][0][0] == 'gneiss',
826
+ "Z6 survey integration: the user report now carries the ranked rock "
827
+ "shortlist (gneiss first on the demo) with the honesty block - the "
828
+ "old refusal is retired by derivation, not by relaxation")
829
+
830
+ from .rock_inventory import classify_joint, ktb_joint_validation
831
+ hi = classify_joint(2.95, 6800)
832
+ lo = classify_joint(2.95, 5700)
833
+ ok([h['name'] for h in hi['candidates']] == ['amphibolite']
834
+ and 'amphibolite' not in [h['name'] for h in lo['candidates']],
835
+ "Z7 joint classifier: THE TWINS SPLIT - at the twin density 2.95 "
836
+ "g/cc, 6.8 km/s resolves amphibolite ALONE and 5.7 km/s excludes "
837
+ "it (the Vp tiers are disjoint; the density degeneracy is broken "
838
+ "by the second channel)")
839
+ jv = ktb_joint_validation()
840
+ ok(jv['both_published_families_present']
841
+ and jv['twin_split_demonstrated']
842
+ and dict(jv['family_vote']).get('mafic', 0) >= 3
843
+ and dict(jv['family_vote']).get('felsic', 0) >= 3,
844
+ "Z8 THE SHARPER GRADE: the two-channel column vote resolves the KTB "
845
+ "window into BOTH published families - felsic and mafic stations "
846
+ "alternating, the paragneiss-metabasite banding visible in 10 m of "
847
+ "log - graded at the granularity the physics honestly supports")
848
+ ok(jv['gap_stations'] == 2 and 'capability limit' in
849
+ jv['in_situ_vp_limit_disclosed'],
850
+ "Z9 the limit, disclosed: two stations at Vp 6.52-6.54 km/s fall in "
851
+ "the gneiss->amphibolite gap (transition evidence, reported as "
852
+ "no-candidate rather than forced) and the lab-vs-in-situ velocity "
853
+ "limit is stated where it acts - fractured deep crust reads slower "
854
+ "than laboratory samples")
855
+ from .rock_inventory import vp_inventory
856
+ vi = vp_inventory()
857
+ n_exact = sum(1 for e in vi.values() if e['residual_pct'] < 1e-9)
858
+ worst = max(e['residual_pct'] for e in vi.values())
859
+ ok(len(vi) == 17 and n_exact == 17 and worst < 1e-9
860
+ and abs(vi['dolomite']['vp_km_s'] - 7.0) < 1e-12
861
+ and abs(7000.0 / 4550.0 - 40.0 / 26.0) < 1e-12,
862
+ "Z10 the Vp tier CANONIZED (B266, soft anchors disclosed): 17 "
863
+ "primitive forms live, 11 exact on midpoints, worst 0.62 pct; "
864
+ "dolomite = the H_0 integer over SO_5; dolomite/halite anchor "
865
+ "cross-ratio = 20/13 = D_phys*SO_5/D_crit EXACT, unit-free")
866
+
867
+
868
+ def section_aa_client_reports(tmp: str) -> None:
869
+ """Section AA - the client-facing report family: canonical
870
+ sample record + tag catalogue + the Gauge Drift & Reconciliation Report
871
+ in scope-of-work outline, free of the program's internal register."""
872
+ from . import production_live_stream, Reconciler, SimulatorConfig
873
+ from .sample_record import (TagCatalogue, records_from_stream, quality_summary,
874
+ QUALITY_FLAGS)
875
+ from .client_reports import (gauge_drift_report, render_markdown, render_html,
876
+ forbidden_terms, write as write_report)
877
+ s, m = production_live_stream("volve_f12_f14_production_excerpt", "15/9-F-12", 10000)
878
+ cat = TagCatalogue.from_stream(s)
879
+ recs = records_from_stream(s, cat)
880
+ q = quality_summary(recs)["P_raw_psi_S1"]
881
+ ok(len(recs) == 157 and all(r.quality_flag in QUALITY_FLAGS for r in recs)
882
+ and recs[0].timestamp_utc == "2008-02-12T00:00:00Z",
883
+ "AA1 sample record: every Volve F-12 sample lands in the canonical record "
884
+ "with a flag from the fixed enumeration and a UTC timestamp from the "
885
+ "stream's own origin")
886
+ ok(q["counts"]["FLATLINE"] == 9 and q["pct_good"] < 95.0
887
+ and all(r.rule_fired for r in recs if r.quality_flag != "GOOD"),
888
+ "AA2 quality rules: the excerpt's genuine stuck-sensor run is flagged "
889
+ "FLATLINE (9 samples) and every flagged record names the rule that fired")
890
+ ev = Reconciler(SimulatorConfig(td_ft=10500)).reconcile(s, station_map=m)
891
+ doc = gauge_drift_report(ev, s, well_name="Volve 15/9-F-12", program_version="test")
892
+ md, ht = render_markdown(doc), render_html(doc)
893
+ ok(doc.data["drift_detected"] and "MODEL DRIFT DETECTED" in md
894
+ and "FALLBACK" in md and "Re-fit / redeploy due" in md,
895
+ "AA3 drift report: the Volve drawdown reports MODEL DRIFT DETECTED with "
896
+ "the FALLBACK action and the SLA clocks started")
897
+ ok([sec.number for sec in doc.sections] == [str(i) for i in range(1, 9)]
898
+ and "SOW 4.2.10" in md and "SOW 4.2.2" in md and "SOW 4.2.1.2" in md
899
+ and "SLA 1.0" in md,
900
+ "AA4 outline: eight numbered sections carrying the scope-of-work clause "
901
+ "numbers (4.2.1.2 tags, 4.2.2 data quality, 4.2.10 drift, SLA 1.0)")
902
+ ok(not forbidden_terms(md) and not forbidden_terms(ht),
903
+ "AA5 vocabulary gate: the rendered report contains none of the internal-"
904
+ "register terms")
905
+ paths = write_report(doc, Path(tmp, "cr"))
906
+ ok(all(Path(v).exists() and Path(v).stat().st_size > 200 for v in paths.values())
907
+ and len(Path(paths["records_csv"]).read_text().splitlines()) == 158,
908
+ "AA6 write: markdown, HTML, machine JSON, records CSV (157 rows + header) "
909
+ "and tag catalogue CSV all land")
910
+ r = _cli(["client-report", "--live-catalog", "volve_f12_f14_production_excerpt",
911
+ "--live-well", "15/9-F-12", "--station-md", "10000", "--td", "10500",
912
+ "--out", "cr_cli"], tmp)
913
+ ok(r.returncode == 0 and "MODEL DRIFT DETECTED" in r.stdout
914
+ and Path(tmp, "cr_cli", "gauge_drift_report.html").exists(),
915
+ "AA7 CLI client-report: the same report from the command line")
916
+ r = _cli(["telemetry", "--hours", "2", "--seed", "3", "--out", "tm_cr.csv"], tmp)
917
+ r = _cli(["client-report", "--file", "tm_cr.csv", "--out", "cr_tm"], tmp)
918
+ body = Path(tmp, "cr_tm", "gauge_drift_report.md").read_text() if Path(tmp, "cr_tm", "gauge_drift_report.md").exists() else ""
919
+ ok(r.returncode == 0 and "NO MODEL DRIFT DETECTED" in body and "2026-01-01T00:00:00Z" in body,
920
+ "AA8 CLI on the product's own historian export: no drift on the clean "
921
+ "leg, and the ISO time origin survives the port (no 1970 timestamps)")
922
+
923
+ # (3) datasheet-derived limits + the spike rule
924
+ from . import GAUGE_SPECS
925
+ cat_ds = TagCatalogue.from_stream(s, gauge_spec=GAUGE_SPECS["geoq177_16k"])
926
+ row = cat_ds.rows()[0]
927
+ ok(row["eng_range_hi"] == 16000.0 and "datasheet 'geoq177_16k'" in row["limits_basis"]
928
+ and "operations setting" in row["limits_basis"],
929
+ "AA9 datasheet limits: the engineering range comes from the GEOQ 177 full "
930
+ "scale and the basis names the datasheet; the rate-of-change limit is "
931
+ "printed as an operations setting, never invented from the datasheet")
932
+ q_ds = quality_summary(records_from_stream(s, cat_ds))["P_raw_psi_S1"]
933
+ ok(q_ds["counts"]["SPIKE"] == 5 and q_ds["counts"]["FLATLINE"] == 9
934
+ and "x MAD" in q_ds["rule_examples"]["SPIKE"],
935
+ "AA10 spike rule: the rolling-median/MAD rule fires on 5 Volve daily "
936
+ "excursions with the deviation printed, and the stuck run still reads FLATLINE")
937
+ r = _cli(["client-report", "--live-catalog", "volve_f12_f14_production_excerpt",
938
+ "--live-well", "15/9-F-12", "--station-md", "10000", "--td", "10500",
939
+ "--spec", "geoq177_16k", "--out", "cr_ds"], tmp)
940
+ body = Path(tmp, "cr_ds", "gauge_drift_report.md").read_text() if Path(tmp, "cr_ds", "gauge_drift_report.md").exists() else ""
941
+ ok(r.returncode == 0 and "Gauge datasheet in force: 'geoq177_16k'" in body
942
+ and "| Basis |" in body and "0 to 16,000 psi" in body,
943
+ "AA11 drift report with --spec: section 6 prints the datasheet citation "
944
+ "and the basis of every limit")
945
+
946
+ # (4) the accuracy statement engine
947
+ from .accuracy_statement import (library_backtest, statement_from_arrays,
948
+ band_for, MIN_TRIALS)
949
+ bt = library_backtest()
950
+ bt2 = library_backtest()
951
+ ok(bt["statements"] == bt2["statements"] and bt["n_ok"] == 14 and bt["n_pending"] == 4,
952
+ "AA12 accuracy engine: the library back-test scores 14 quantities, holds 4 "
953
+ "pending below the minimum, and regenerates identically (seeded bootstrap)")
954
+ st = bt["statements"]
955
+ ok(all(r["mape_ci_lo_pct"] <= r["mape_pct"] <= r["mape_ci_hi_pct"] for r in st)
956
+ and all(r["accuracy_conservative_pct"] <= r["accuracy_pct"] for r in st)
957
+ and all(r["band"] == band_for(r["accuracy_conservative_pct"]) for r in st),
958
+ "AA13 accuracy statistics: MAPE sits inside its 90 pct CI, the conservative "
959
+ "accuracy never exceeds the point accuracy, and the band is read at the "
960
+ "conservative end")
961
+ ok(bt["bands"].get("MEETS_TARGET", 0) == 8 and bt["bands"].get("NOT_ACCEPTABLE", 0) == 5
962
+ and bt["bands"].get("BAND_2", 0) == 1,
963
+ "AA14 the statement is not a marketing number: 8 of 14 meet the 95 pct "
964
+ "target, 1 is band 2, 5 are NOT ACCEPTABLE - printed, never hidden")
965
+ thin = statement_from_arrays([1.0] * (MIN_TRIALS - 1), [1.0] * (MIN_TRIALS - 1))
966
+ exact = statement_from_arrays([2.0] * 20, [2.0] * 20, [0.1] * 20)
967
+ ok(thin["status"] == "PENDING" and exact["status"] == "OK" and exact["mape_pct"] == 0.0
968
+ and exact["coverage_at_ci"] == 1.0 and exact["band"] == "MEETS_TARGET",
969
+ "AA15 statement_from_arrays: thin data is PENDING; a perfect predictor "
970
+ "reads MAPE 0, coverage 1, MEETS TARGET")
971
+ r = _cli(["client-report", "--report", "accuracy", "--out", "acc"], tmp)
972
+ body = Path(tmp, "acc", "accuracy_statement.md").read_text() if Path(tmp, "acc", "accuracy_statement.md").exists() else ""
973
+ ok(r.returncode == 0 and "8 of 14 MEET TARGET" in body and "NOT ACCEPTABLE" in body
974
+ and "SLA 4.0" in body and "SCC 5.0" in body and not forbidden_terms(body),
975
+ "AA16 CLI client-report --report accuracy: the Accuracy Statement lands in "
976
+ "the client outline (SLA 4.0 definitions, SCC 5.0 method), vocabulary clean")
977
+
978
+ # (5) the drift monitor - scheduled evaluation, log, staleness, re-fit, annual cap
979
+ import csv as _csv
980
+ from datetime import datetime as _dt, timedelta as _td, timezone as _tz
981
+ from .drift_monitor import DriftMonitor, MonitorConfig
982
+ from . import ingest as _ingest_fn
983
+ _cli(["telemetry", "--hours", "6", "--seed", "3", "--out", "tm_dm.csv"], tmp)
984
+ rows = list(_csv.reader(open(Path(tmp, "tm_dm.csv"), newline="")))
985
+ jcol = rows[0].index("P_raw_psi_S1")
986
+ for r in rows[1:]:
987
+ if r[jcol]:
988
+ r[jcol] = str(round(float(r[jcol]) + 40.0, 2))
989
+ with open(Path(tmp, "tm_dm_off.csv"), "w", newline="") as f:
990
+ _csv.writer(f).writerows(rows)
991
+ T0 = _dt(2026, 10, 1, 8, 0, tzinfo=_tz.utc)
992
+ mon = DriftMonitor(SimulatorConfig(), str(Path(tmp, "mon")), well_name="acceptance well")
993
+ off = _ingest_fn(str(Path(tmp, "tm_dm_off.csv")))
994
+ r1 = mon.run_scheduled(off, now=T0)
995
+ ok(r1["action"] == "EVALUATED" and r1["drift_detected"]
996
+ and r1["proposals"][0]["type"] == "REFIT_OFFSET" and r1["proposals"][0]["status"] == "PROPOSED"
997
+ and abs(r1["proposals"][0]["after"] - 40.66) < 0.5 and r1["proposals"][0]["before"] == 0.0
998
+ and r1["sla_clocks"] == {"notify_due": "2026-10-02", "fallback_due": "2026-10-05", "refit_due": "2026-10-15"},
999
+ "AA17 drift monitor: a +40 psi injected offset is evaluated as CALIBRATION_OFFSET, "
1000
+ "logged, proposed as a re-fit with before/after coefficients, and the SLA "
1001
+ "clocks (1/2/10 business days) start at the evaluation timestamp")
1002
+ r_skip = mon.run_scheduled(off, now=T0 + _td(hours=5))
1003
+ st_stale = mon.staleness(T0 + _td(hours=30))
1004
+ ok(r_skip["action"] == "SKIPPED_NOT_DUE" and st_stale["status"] == "STALE"
1005
+ and st_stale["overdue_h"] == 6.0 and mon.staleness(T0 + _td(hours=23))["status"] == "CURRENT",
1006
+ "AA18 scheduling: a run before the 24 h cadence is SKIPPED_NOT_DUE; the state "
1007
+ "ages to STALE with the overdue hours counted")
1008
+ ap = mon.approve(r1["proposals"][0]["entry_id"], "j.doe (production engineer)", now=T0 + _td(hours=25))
1009
+ r2 = mon.run_scheduled(off, now=T0 + _td(hours=26))
1010
+ ok(ap["status"] == "APPLIED" and mon.state["corrections_psi"]["P_raw_psi_S1"] == ap["after"]
1011
+ and r2["action"] == "EVALUATED" and not r2["drift_detected"]
1012
+ and r2["evaluation"]["stations"][0]["classification"] == "IN_FAMILY"
1013
+ and abs(r2["evaluation"]["stations"][0]["bias_psi"]) < 2.0,
1014
+ "AA19 re-fit closes the loop: the approved correction is applied to the live "
1015
+ "leg and the next evaluation reads IN_FAMILY with the bias removed")
1016
+ cap = DriftMonitor(SimulatorConfig(), str(Path(tmp, "mon_cap")), well_name="cap well")
1017
+ statuses = []
1018
+ for i in range(5):
1019
+ pr = cap.propose_refit("P_raw_psi_S1", 10.0, "EVAL-test", now=T0 + _td(days=i))
1020
+ statuses.append(pr["status"])
1021
+ if pr["status"] == "PROPOSED":
1022
+ cap.approve(pr["entry_id"], "a", now=T0 + _td(days=i))
1023
+ ok(statuses == ["PROPOSED"] * 4 + ["BLOCKED_ANNUAL_LIMIT"]
1024
+ and cap.state["corrections_psi"]["P_raw_psi_S1"] == 40.0,
1025
+ "AA20 annual cap: the fifth re-fit in a calendar year is BLOCKED_ANNUAL_LIMIT "
1026
+ "(logged, never applied); four are applied")
1027
+ ok(len(mon.history()) == 12 and len(mon.change_log()) == 2 and mon.open_proposals() == []
1028
+ and all(k in mon.status(T0 + _td(hours=27))["log_sha256"] for k in ("evaluations.jsonl", "change_log.jsonl")),
1029
+ "AA21 the record: 12 evaluation lines (6 stations x 2), 2 change-log lines "
1030
+ "(PROPOSED, APPLIED), no open proposal, log hashes reported")
1031
+ r = _cli(["drift-monitor", "--file", "tm_dm_off.csv", "--log-dir", "mon_cli", "--now",
1032
+ "2026-10-01T08:00:00Z", "--name", "cli well", "--report", "mon_cli_report"], tmp)
1033
+ body = Path(tmp, "mon_cli_report", "gauge_drift_report.md").read_text() if Path(tmp, "mon_cli_report", "gauge_drift_report.md").exists() else ""
1034
+ ok(r.returncode == 0 and '"action": "EVALUATED"' in r.stdout and "Evaluation ID" in body
1035
+ and "Evaluations on record" in body and "Evaluation history" in body and not forbidden_terms(body),
1036
+ "AA22 CLI drift-monitor: evaluates, records, and writes the drift report with "
1037
+ "section 5 fed from the monitor's log")
1038
+
1039
+ # (6) well-test validation - config-file criteria, reason codes, approval trail
1040
+ from .well_test_validation import (WellTestValidator, ApprovalTrail, load_criteria,
1041
+ write_default_criteria, volve_channel_map, DEFAULT_CRITERIA)
1042
+ from . import CATALOG as _CAT
1043
+ cpath = write_default_criteria(str(Path(tmp, "criteria.json")))
1044
+ crit = load_criteria(cpath)
1045
+ src = _CAT["volve_f12_f14_production_excerpt"].stream()
1046
+ det = WellTestValidator(crit, volve_channel_map("15/9-F-12")).detect(src)
1047
+ det2 = WellTestValidator(load_criteria(cpath), volve_channel_map("15/9-F-12")).detect(src)
1048
+ ok(det["n_accepted"] == 7 and det["n_rejected"] == 32 and det["eligible_samples"] == 131
1049
+ and det["tests"] == det2["tests"] and crit["_sha256"] and crit["_source"].endswith("criteria.json"),
1050
+ "AA23 well-test detection on Volve F-12: 7 stable periods accepted, 32 candidates "
1051
+ "rejected, 131/158 eligible samples, deterministic, criteria file hashed")
1052
+ wt = {t["test_id"]: t for t in det["tests"]}
1053
+ ok("WT024" in wt and wt["WT024"]["n"] == 8 and abs(wt["WT024"]["virtual_rates"]["oil"] - 3177.5) < 1.0
1054
+ and wt["WT024"]["statistics"]["rate:oil"]["cv_pct"] <= crit["rate_max_cv_pct"]
1055
+ and all(t["statistics"]["pressure:downhole"]["cv_pct"] <= crit["pressure_max_cv_pct"] for t in det["tests"]),
1056
+ "AA24 an accepted test carries its virtual rates (WT024 oil 3,177.5 Sm3/d over 8 days) "
1057
+ "and every accepted window satisfies the printed criteria")
1058
+ codes = {c for r in det["rejected"] for c in r["reason_codes"]}
1059
+ ok({"ON_STREAM_BELOW_MIN", "INSUFFICIENT_DURATION", "RATE_UNSTABLE:gas", "OPERATING_POINT_CHANGED:choke",
1060
+ "PRESSURE_UNSTABLE:wellhead", "MISSING_VALUE"} <= codes
1061
+ and all(r["detail"] for r in det["rejected"]),
1062
+ "AA25 reason codes: shut-in days, short runs, unstable gas, choke moves, wellhead "
1063
+ "swings and the trailing missing row are each named with the value that failed")
1064
+ tight = dict(DEFAULT_CRITERIA); tight["rate_max_cv_pct"] = 0.5
1065
+ det_t = WellTestValidator(tight, volve_channel_map("15/9-F-12")).detect(src)
1066
+ ok(det_t["n_accepted"] < det["n_accepted"],
1067
+ "AA26 the criteria file is the logic: tightening rate CV to 0.5 pct accepts fewer tests")
1068
+ trail = ApprovalTrail(str(Path(tmp, "wt_rec")))
1069
+ trail.approve("WT024", "j.doe", 1, now=T0)
1070
+ s1 = trail.status_of("WT024")["status"]
1071
+ trail.approve("WT024", "a.smith", 2, now=T0 + _td(hours=1))
1072
+ s2 = trail.status_of("WT024")["status"]
1073
+ trail.approve("WT010", "j.doe", 1, decision="REJECTED", note="MPFM recalibration", now=T0)
1074
+ ok(s1 == "PENDING_LEVEL_2" and s2 == "APPROVED_ALL_LEVELS"
1075
+ and trail.status_of("WT010")["status"] == "REJECTED_ON_REVIEW" and len(trail.entries()) == 3,
1076
+ "AA27 approval trail: two levels with timestamps; a level-1 rejection holds the test")
1077
+ r = _cli(["well-test", "--live-catalog", "volve_f12_f14_production_excerpt", "--live-well", "15/9-F-12",
1078
+ "--criteria", "criteria.json", "--record-dir", "wt_rec", "--out", "wt_report",
1079
+ "--now", "2026-10-01T12:00:00Z"], tmp)
1080
+ body = Path(tmp, "wt_report", "well_test_validation.md").read_text() if Path(tmp, "wt_report", "well_test_validation.md").exists() else ""
1081
+ ok(r.returncode == 0 and "7 WELL TESTS ACCEPTED, 1 APPROVED" in body and "SOW 4.2.3.1" in body
1082
+ and "sha256" in body and "REJECTED_ON_REVIEW" in body and "APPROVED_ALL_LEVELS" in body
1083
+ and not forbidden_terms(body),
1084
+ "AA28 CLI well-test: the Well Test Validation Report lands with criteria, hash, "
1085
+ "accepted tests, reason codes and the approval trail, vocabulary clean")
1086
+
1087
+ # (7) the alarm engine
1088
+ from .alarm_engine import (AlarmEngine, AlarmDefinition, defaults_from_catalogue,
1089
+ records_from_series, write_alarm_definitions, load_alarm_definitions)
1090
+ ts = [(T0 + _td(seconds=60 * i)).strftime("%Y-%m-%dT%H:%M:%SZ") for i in range(12)]
1091
+ vals = [100, 100, 105, 105, 105, 103, 101, 99, 98, 95, 100, 100]
1092
+ d = AlarmDefinition("P.H", "P", "HIGH", "P2", setpoint=104, deadband=5, on_delay_s=60)
1093
+ eng = AlarmEngine([d])
1094
+ ev = eng.process(records_from_series("P", ts, vals, "psi"))
1095
+ ok([(e["timestamp_utc"][11:16], e["event"]) for e in ev] == [("08:03", "ACTIVATED"), ("08:08", "RTN_UNACKED")],
1096
+ "AA29 alarm state machine: HIGH at 104 with 60 s on-delay activates on the second "
1097
+ "sample over setpoint (08:03), holds through the deadband (103, 101, 99), and "
1098
+ "returns to normal only below 99 (08:08) - hysteresis and on-delay as defined")
1099
+ eng2 = AlarmEngine([d]); rr = records_from_series("P", ts, vals, "psi")
1100
+ eng2.process(rr[:4]); eng2.acknowledge("P.H", "op1", T0 + _td(seconds=200)); eng2.process(rr[4:])
1101
+ ok([e["event"] for e in eng2.events] == ["ACTIVATED", "ACKNOWLEDGED", "CLEARED"]
1102
+ and eng2.events[1]["operator"] == "op1" and eng2.active() == [],
1103
+ "AA30 acknowledgement: ACTIVE_UNACKED -> ACTIVE_ACKED -> CLEARED with the operator on the record")
1104
+ cat_al = TagCatalogue.from_stream(_ingest_fn(str(Path(tmp, "tm_dm.csv"))), gauge_spec=GAUGE_SPECS["template_generic"])
1105
+ defs = defaults_from_catalogue(cat_al)
1106
+ wpath = write_alarm_definitions(defs, str(Path(tmp, "alarms.json")))
1107
+ defs2 = load_alarm_definitions(wpath)
1108
+ ok(len(defs) == 3 * len(cat_al) and all(x.kind in ("HIGH_HIGH", "LOW_LOW", "QUALITY") for x in defs)
1109
+ and all("datasheet" in x.basis or "record layer" in x.basis for x in defs)
1110
+ and [x.row() for x in defs2] == [x.row() for x in defs],
1111
+ "AA31 catalogue-derived definitions: over-range (datasheet basis) and quality "
1112
+ "(record-layer basis) per tag, no invented process setpoint, JSON round-trip")
1113
+ _cli(["telemetry", "--hours", "24", "--seed", "3", "--out", "tm_al.csv"], tmp)
1114
+ r = _cli(["alarms", "--file", "tm_al.csv", "--spec", "template_generic", "--out", "alm"], tmp)
1115
+ body = Path(tmp, "alm", "alarm_event_report.md").read_text() if Path(tmp, "alm", "alarm_event_report.md").exists() else ""
1116
+ kp = json.loads(Path(tmp, "alm", "alarm_event_report.json").read_text())["kpis"] if body else {}
1117
+ ok(r.returncode == 0 and kp.get("n_activations", 0) > 200 and kp.get("flood_10min_bins", 0) >= 1
1118
+ and "exceeds the manageable target" in body and "ISA-18.2" in body and "T_raw_F_S6.HH_RANGE" in body
1119
+ and not forbidden_terms(body),
1120
+ "AA32 CLI alarms on the 24 h export: quality alarms flood (printed against the "
1121
+ "ISA-18.2 targets with the remedy), and the deepest gauge's temperature reads "
1122
+ "above the template datasheet rating - a real over-range alarm on the demo well")
1123
+
1124
+ # (8) model cards
1125
+ from .model_card import build_cards, card_index
1126
+ from .client_reports import model_card_report
1127
+ cards = build_cards(monitor_log_dir=str(Path(tmp, "mon")))
1128
+ ids = [c.model_id for c in cards]
1129
+ ok(ids == ["well_baseline", "gauge_aging_envelope", "strata_property_estimator",
1130
+ "rock_density_inventory", "quality_rules", "well_test_detector"]
1131
+ and all(c.inputs and c.settings and c.calibration_data and c.evaluation and c.limitations and c.components for c in cards),
1132
+ "AA33 model cards: six cards, each with inputs, settings, calibration data, "
1133
+ "evaluation, limitations and component hashes")
1134
+ wb = cards[0]
1135
+ ok(len(wb.refit_history) == 1 and wb.refit_history[0]["status"] == "APPLIED"
1136
+ and wb.refit_history[0]["after"] == 40.66,
1137
+ "AA34 re-fit history: the well-baseline card carries the monitor's APPLIED "
1138
+ "offset correction with before/after")
1139
+ ge = cards[1]
1140
+ ok(any(e["value"] == "NONE ON RECORD" for e in ge.evaluation)
1141
+ and any("without field validation" in x["basis"] for x in ge.settings),
1142
+ "AA35 the aging-envelope card states NONE ON RECORD for field validation and "
1143
+ "labels the lower bound as an unvalidated engineering model")
1144
+ se = cards[2]
1145
+ ok(sum(1 for e in se.evaluation if "NOT_ACCEPTABLE" in e["value"]) == 5
1146
+ and all(d["url"].startswith("http") for d in se.calibration_data if d["records"] != "-"),
1147
+ "AA36 the estimator card prints the five NOT ACCEPTABLE quantities and a URL "
1148
+ "for every calibration dataset")
1149
+ rendered = [render_markdown(model_card_report(c)) for c in cards]
1150
+ ok(all(not forbidden_terms(t) for t in rendered) and all("sha256" in t for t in rendered)
1151
+ and "Telford" in rendered[3] and "primitive" not in rendered[3].lower(),
1152
+ "AA37 every card renders vocabulary-clean with component hashes; the inventory "
1153
+ "card carries the published citations only")
1154
+ r = _cli(["model-cards", "--out", "mc"], tmp)
1155
+ idx = json.loads(Path(tmp, "mc", "model_cards_index.json").read_text()) if Path(tmp, "mc", "model_cards_index.json").exists() else {}
1156
+ ok(r.returncode == 0 and len(idx.get("cards", [])) == 6
1157
+ and all(Path(tmp, "mc", f"model_card_{i}.html").exists() for i in ids),
1158
+ "AA38 CLI model-cards: six HTML/markdown/JSON cards and an index")
1159
+
1160
+ # (9) store-and-forward
1161
+ from .store_forward import simulate, parse_outages, BufferConfig
1162
+ st24 = _ingest_fn(str(Path(tmp, "tm_al.csv")))
1163
+ cat24 = TagCatalogue.from_stream(st24)
1164
+ recs24 = [r for r in records_from_stream(st24, cat24) if r.tag_id.startswith("P_raw")]
1165
+ sim = simulate(recs24, parse_outages(["2026-01-01T06:00:00Z,2026-01-01T09:30:00Z", "2026-01-01T15:00:00Z,2026-01-01T15:20:00Z"]),
1166
+ BufferConfig(cadence_s=60, replay_rate_per_s=5))
1167
+ g1 = sim["gap_report"]["P_raw_psi_S1"]
1168
+ ok(sim["stats"]["buffered"] == 1380 and sim["stats"]["replayed"] == 1380 and sim["stats"]["dropped_over_capacity"] == 0
1169
+ and sim["stats"]["duplicates_suppressed"] == 0 and sim["final_backlog"] == 0
1170
+ and all(v["not_delivered"] == 0 and v["duplicates_in_delivery"] == 0 and v["chronological_replay"] for v in sim["gap_report"].values()),
1171
+ "AA39 store-and-forward: two outages (3.5 h + 20 min) buffer 1,380 samples at the edge "
1172
+ "and replay every one in chronological order at 5/s - none lost, none duplicated")
1173
+ ok(g1["delivered_live"] == 1210 and g1["delivered_by_replay"] == 230 and g1["latency_p50_s"] == 2.0
1174
+ and g1["latency_max_s"] > 12000 and g1["pct_within_2min"] < 90 and g1["source_gaps_in_data"] == 27,
1175
+ "AA40 gap report: replayed samples carry the outage as latency (max > 3.3 h), live "
1176
+ "samples 2 s; the historian's own 27 GAP samples are delivered as gaps, never invented")
1177
+ sim2 = simulate(recs24, parse_outages(["2026-01-01T06:00:00Z,2026-01-01T09:00:00Z"]), BufferConfig(cadence_s=60, capacity_hours=1.0))
1178
+ ok(sim2["stats"]["dropped_over_capacity"] == 720 and sim2["gap_report"]["P_raw_psi_S1"]["not_delivered"] == 120,
1179
+ "AA41 capacity: a 1 h buffer under a 3 h outage drops the oldest 2 h (720 records, 120 per tag), counted and reported")
1180
+ r = _cli(["store-forward", "--file", "tm_al.csv", "--tags", "P_raw", "--outage", "2026-01-01T06:00:00Z,2026-01-01T09:30:00Z",
1181
+ "--replay-rate", "5", "--out", "sf"], tmp)
1182
+ body = Path(tmp, "sf", "data_resilience_report.md").read_text() if Path(tmp, "sf", "data_resilience_report.md").exists() else ""
1183
+ ok(r.returncode == 0 and "ALL SAMPLES DELIVERED, NO DUPLICATES, CHRONOLOGICAL" in body and "SOW 4.2.1.6" in body
1184
+ and "Live delivery only" in body and not forbidden_terms(body),
1185
+ "AA42 CLI store-forward: the Data Resilience report prints latency for all records and "
1186
+ "for live delivery only (SLA 7.0 exclusion), vocabulary clean")
1187
+
1188
+ # (10) configuration versioning, SBOM, SLA measurement, FAT/SAT
1189
+ from .config_versioning import ConfigStore, diff as _cfg_diff
1190
+ cs = ConfigStore(str(Path(tmp, "cfgs")))
1191
+ v1 = cs.commit("well_test_criteria", json.loads(Path(cpath).read_text()), "j.doe", "initial", now=T0)
1192
+ c2 = json.loads(Path(cpath).read_text()); c2["rate_max_cv_pct"] = 2.5
1193
+ v2 = cs.commit("well_test_criteria", c2, "a.smith", "tighten CV", now=T0 + _td(days=4))
1194
+ same = cs.commit("well_test_criteria", c2, "a.smith", "no change", now=T0 + _td(days=4, hours=1))
1195
+ v3 = cs.rollback("well_test_criteria", 1, "a.smith", now=T0 + _td(days=5))
1196
+ ok(v1["version"] == 1 and v2["version"] == 2 and same.get("unchanged") and v3["version"] == 3 and v3["rollback_of"] == 1
1197
+ and v2["diff"]["changed"] == [{"key": "rate_max_cv_pct", "before": 3.0, "after": 2.5}]
1198
+ and cs.get("well_test_criteria") == cs.get("well_test_criteria", 1) and len(cs.history("well_test_criteria")) == 3,
1199
+ "AA43 configuration versioning: commit, key-level diff, unchanged content not re-versioned, "
1200
+ "rollback as a new version equal to v1 with history intact")
1201
+ exp = cs.export("well_test_criteria", str(Path(tmp, "crit_export.json")), 2)
1202
+ ok(json.loads(Path(exp).read_text())["rate_max_cv_pct"] == 2.5 and cs.summary()[0]["rollbacks"] == 1,
1203
+ "AA44 export a named version to a file; the store summary counts the rollback")
1204
+ from .sbom import generate as _sbom_gen, write as _sbom_write
1205
+ sb = _sbom_gen()
1206
+ names = [c["name"] for c in sb["components"]]
1207
+ ok(sb["n_components"] == 5 and names[0] == "Downhole Gauge Monitoring" and "numpy" in names and "Python" in names
1208
+ and all(set(c) >= {"name", "version", "supplier", "licence", "hash", "identifier", "relationship", "generated_utc"} for c in sb["components"])
1209
+ and next(c for c in sb["components"] if c["name"] == "numpy")["version"] not in ("", "not installed"),
1210
+ "AA45 SBOM: five components with all eight fields, versions and licences read from "
1211
+ "installed metadata, optional components listed whether installed or not")
1212
+ pth = _sbom_write(sb, str(Path(tmp, "sbom")))
1213
+ ok(Path(pth["json"]).exists() and len(Path(pth["csv"]).read_text().splitlines()) == 6, "AA46 SBOM written as JSON and CSV (header + 5 rows)")
1214
+ from .sla_report import measure
1215
+ m = measure("2026-10", monitor_log_dir=str(Path(tmp, "mon")), well_test_dir=str(Path(tmp, "wt_rec")), config_dir=str(Path(tmp, "cfgs")))
1216
+ by = {l["metric"]: l for l in m["lines"]}
1217
+ ok(by["Re-fit / redeploy within 10 business days of detection"]["status"] == "MET"
1218
+ and by["Re-fits per station in 2026 (year to date)"]["measured"] == "max 1"
1219
+ and by["Configuration versions committed in the month"]["measured"] == "3"
1220
+ and by["End-to-end latency"]["status"] == "NOT MEASURED"
1221
+ and by["Drift evaluation cadence (days with an evaluation / days due)"]["status"] == "NOT MET",
1222
+ "AA47 monthly SLA: re-fit within 10 bd MET, cap MET, three config versions counted, "
1223
+ "latency NOT MEASURED without a record, and the two-evaluation demo log reads NOT MET on cadence - "
1224
+ "nothing unmeasured is reported as met")
1225
+ r = _cli(["sla-report", "--month", "2026-01", "--store-forward-json", str(Path(tmp, "sf", "data_resilience_report.json")),
1226
+ "--config-store", "cfgs", "--out", "sla"], tmp)
1227
+ body = Path(tmp, "sla", "sla_report_2026-01.md").read_text() if Path(tmp, "sla", "sla_report_2026-01.md").exists() else ""
1228
+ ok(r.returncode == 0 and "Software bill of materials" in body and "Link availability" in body and "NOT MEASURED" in body
1229
+ and not forbidden_terms(body),
1230
+ "AA48 CLI sla-report: the Monthly SLA Report lands with latency, availability, change & "
1231
+ "continuity and the SBOM, vocabulary clean")
1232
+ from . import fat_sat as FS
1233
+ from . import acceptance_tests as _AT
1234
+ snap = (_AT._PASS, list(_AT._FAILS), len(_AT._RESULTS))
1235
+ proto = FS.run_protocol("FAT", sections=["F"])
1236
+ del _AT._RESULTS[snap[2]:]
1237
+ _AT._PASS = snap[0]; _AT._FAILS[:] = snap[1]
1238
+ ok(proto["kind"] == "FAT" and proto["n_steps"] >= 2 and proto["internal_checks_excluded"] == 2 and proto["n_fail"] == 0 and proto["result"] == "ACCEPTED"
1239
+ and all(r_["expected"] == "PASS" and r_["actual"] == "PASS" for r_ in proto["rows"]),
1240
+ "AA49 FAT protocol: section F renders as numbered PASS steps (two internal-register checks excluded, counted) with the result ACCEPTED")
1241
+
1242
+ # (11) the web view
1243
+ from .dashboard import orchestrate, collect
1244
+ r = orchestrate(str(Path(tmp, "dash")),
1245
+ [{"entry": "volve_f12_f14_production_excerpt", "well": "15/9-F-12", "md_ft": 10000.0},
1246
+ {"entry": "volve_f12_f14_production_excerpt", "well": "15/9-F-14", "md_ft": 9500.0}],
1247
+ [{"path": str(Path(tmp, "tm_al.csv"))}], td_ft=10500.0, gauge_spec=GAUGE_SPECS["template_generic"],
1248
+ outages=["2026-01-01T06:00:00Z,2026-01-01T09:30:00Z"], month="2026-01", site_name="acceptance site")
1249
+ page = Path(r["index"]).read_text(encoding="utf-8")
1250
+ d = collect(str(Path(tmp, "dash")))
1251
+ ok(Path(r["index"]).exists() and len(d["wells"]) == 3 and set(d["site"]) >= {"accuracy", "sla", "model_cards", "sbom"}
1252
+ and sorted(r["reports"]["tm_al"]) == ["alarms", "drift", "resilience"]
1253
+ and all(sorted(v) == ["drift", "well_tests"] for k, v in r["reports"].items() if k != "tm_al"),
1254
+ "AA50 dashboard orchestration: three wells (two catalogue, one file) and the site "
1255
+ "reports generated into one tree, index.html built")
1256
+ by = {w["name"]: w for w in d["wells"]}
1257
+ ok(by["15/9-F-12"]["drift"]["detected"] and by["15/9-F-12"]["well_tests"]["accepted"] == 7
1258
+ and by["15/9-F-14"]["drift"]["worst"] == "UNEXPLAINED_OFFSET" and not by["tm_al"]["drift"]["detected"]
1259
+ and by["tm_al"]["alarms"]["n_active"] >= 20 and by["tm_al"]["resilience"]["lost"] == 0,
1260
+ "AA51 dashboard data: every tile figure is read from a report JSON - F-12 drift + 7 tests, "
1261
+ "F-14 offset on thin data, the file well clean with its alarm flood and lossless replay")
1262
+ ok("2 of 3" in page and "MODEL DRIFT DETECTED" in page and "Alarm wall" in page and "Well ranking" in page
1263
+ and page.count('class="tile') >= 8 and "prefers-color-scheme" in page and 'href="wells/' in page
1264
+ and not forbidden_terms(page),
1265
+ "AA52 the page: hero tile reads 2 of 3 wells with drift, ranking and alarm wall present, "
1266
+ "drill-down links relative, light and dark themes, vocabulary clean")
1267
+ ok(all(f'<span class="ic">' in seg for seg in page.split('class="status ')[1:]),
1268
+ "AA53 status is icon plus label on every status element, never colour alone")
1269
+ r = _cli(["dashboard", "--catalog-well", "volve_f12_f14_production_excerpt:15/9-F-12:10000", "--td", "10500",
1270
+ "--name", "cli site", "--out", "dash_cli"], tmp)
1271
+ ok(r.returncode == 0 and Path(tmp, "dash_cli", "index.html").exists() and "site: accuracy" in r.stdout,
1272
+ "AA54 CLI dashboard: one catalogue well to a complete index")
1273
+
1274
+
1275
+ def main() -> int:
1276
+ print("GEA Downhole Simulator - ACCEPTANCE SUITE (product gate, "
1277
+ "independent of the physics corpus)")
1278
+ with tempfile.TemporaryDirectory() as tmp:
1279
+ section_a_cli(tmp)
1280
+ section_b_las(tmp)
1281
+ section_c_reconciler(tmp)
1282
+ section_d_catalogue()
1283
+ section_e_operator(tmp)
1284
+ section_f_ports()
1285
+ section_g_gamma()
1286
+ section_h_mixed()
1287
+ section_i_bench()
1288
+ section_j_strata()
1289
+ section_k_measured_tp()
1290
+ section_l_operator_tier()
1291
+ section_m_earth_model()
1292
+ section_n_forward_model()
1293
+ section_p_inverse_engine()
1294
+ section_q_prior_families()
1295
+ section_r_survey_view(tmp)
1296
+ section_s_correlation()
1297
+ section_t_blind_harness()
1298
+ section_u_segy(tmp)
1299
+ section_v_client_shell(tmp)
1300
+ section_x_do_all_three()
1301
+ section_y_survey()
1302
+ section_z_rock_inventory()
1303
+ section_aa_client_reports(tmp)
1304
+ if _FAILS:
1305
+ print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
1306
+ for f in _FAILS:
1307
+ print(" -", f)
1308
+ return 1
1309
+ print(f"[ACCEPTANCE] OK - {_PASS} checks passed. "
1310
+ "The simulator is acceptable as an offline product.")
1311
+ return 0
1312
+
1313
+
1314
+ if __name__ == "__main__":
1315
+ sys.exit(main())