P0 falsification harness (H1-H4) + Forgejo CI wiring
Some checks are pending
vep-p1-ci / verify (push) Waiting to run
Some checks are pending
vep-p1-ci / verify (push) Waiting to run
This commit is contained in:
parent
0ce1592688
commit
191ae15c98
4 changed files with 186 additions and 3 deletions
|
|
@ -36,8 +36,10 @@ VERIFIED (real execution, not claims)
|
|||
OUTSTANDING (requires USER action — external)
|
||||
=====================================================================
|
||||
- DNS: add A records for api.mangoopsdesign.com and git.mangoopsdesign.com -> 177.7.40.244.
|
||||
Once they resolve publicly, Caddy auto-issues Let's Encrypt certs and the edge goes live
|
||||
on real TLS. Until then, Caddy logs ACME NXDOMAIN errors (harmless; retries).
|
||||
DONE 2026-08-07. Caddy auto-issued Let's Encrypt certs; edge verified live over real TLS
|
||||
(api./health -> ok; git./ -> 200 Forgejo; POST /events over public TLS -> recorded).
|
||||
Note: the box's own local resolver cached the old NXDOMAIN briefly; external clients
|
||||
(public DNS) reach the edge correctly. No action needed.
|
||||
- Secrets: .env uses dev passwords. Before any real tenant, set strong PG/vep_app secrets
|
||||
and consider moving Forgejo to postgres at P6.
|
||||
- P0 falsification (gst_p0_falsification.md) still not run — per the spine, P0 should have
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ services:
|
|||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:10.0.3
|
||||
environment:
|
||||
FORGEJO__database__DB_TYPE: sqlite
|
||||
FORGEJO__database__DB_TYPE: sqlite3
|
||||
FORGEJO__server__SSH_PORT: 22
|
||||
FORGEJO__server__ROOT_URL: https://git.mangoopsdesign.com
|
||||
FORGEJO__server__APP_DATA_PATH: /data
|
||||
|
|
|
|||
161
scripts/p0/run.py
Normal file
161
scripts/p0/run.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# VEP — Phase 0 Falsification Harness (GST reference tenant)
|
||||
#
|
||||
# PURPOSE
|
||||
# Run the four load-bearing GST hypotheses (H1-H4) against real data and emit a
|
||||
# KILL / KEEP / REVISE verdict per the gst_p0_falsification.md worksheet.
|
||||
# This is the gate that, per the spine, should have preceded the P1 build. Since P1
|
||||
# exists, this harness lets the gate run now: supply data -> get verdicts -> prune or keep.
|
||||
#
|
||||
# DESIGN RULES (spine-consistent)
|
||||
# - No fabricated numbers. If data is missing, the verdict is INSUFFICIENT, never a guess.
|
||||
# - Each hypothesis names its falsify-condition and the data column(s) it needs.
|
||||
# - Verdicts are explainable: every KILL cites the observed number that killed it.
|
||||
#
|
||||
# USAGE
|
||||
# python scripts/p0/run.py --data-dir scripts/p0/sample_data
|
||||
# Data files expected (see templates in sample_data/README):
|
||||
# signups.csv : email, intent_bucket, converted_to_foundation(bool), date
|
||||
# cohort_jan2026.csv : attendee, bought_ageless(bool), upgraded_tier2(bool)
|
||||
# subscribers.csv : sub_id, tier, practice_signal(0-1), continued(bool), purchase_recency_days
|
||||
# capped_clients.csv : client_id, revenue_stream, converted_to_scalable(bool)
|
||||
#
|
||||
# With no data the harness reports INSUFFICIENT for every hypothesis (honest default).
|
||||
|
||||
from __future__ import annotations
|
||||
import argparse, csv, json, os, sys
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class Hypothesis:
|
||||
id: str
|
||||
statement: str
|
||||
needs: str
|
||||
falsify: str
|
||||
verdict: str = "INSUFFICIENT"
|
||||
evidence: str = "no data supplied"
|
||||
|
||||
def report(self) -> str:
|
||||
return f"[{self.id}] {self.verdict}\n claim: {self.statement}\n needs: {self.needs}\n falsify-if: {self.falsify}\n evidence: {self.evidence}\n"
|
||||
|
||||
|
||||
HYPS = [
|
||||
Hypothesis("H1", "Intent-based routing + Foundation Series landing converts the 190-signup audience at a rate worth building funnel infra for.",
|
||||
"signups.csv (intent_bucket, converted_to_foundation)",
|
||||
"conversion < threshold needed to justify build (define threshold before run)"),
|
||||
Hypothesis("H2", "Behavioral recency (practice signal) predicts subscription continuation better than 90/180-day purchase timers.",
|
||||
"subscribers.csv (practice_signal, continued, purchase_recency_days)",
|
||||
"purchase_recency correlates with continuation >= practice_signal"),
|
||||
Hypothesis("H3", "Movement Lab as qualification hits Ageless Body 25%+ / Tier 2 40%+ conversion.",
|
||||
"cohort_jan2026.csv (bought_ageless, upgraded_tier2)",
|
||||
"actual conversion materially below targets, or Lab adds no lift vs non-attendees"),
|
||||
Hypothesis("H4", "Founder-time-capped revenue (56%) shifts to scalable streams via acquisition instrumentation alone, without practitioner training.",
|
||||
"capped_clients.csv (revenue_stream, converted_to_scalable)",
|
||||
"capped clients do not convert to scalable streams at the needed rate"),
|
||||
]
|
||||
|
||||
|
||||
def _load(path: str) -> list[dict]:
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
with open(path, newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def _rate(rows, col) -> float | None:
|
||||
if not rows:
|
||||
return None
|
||||
n = sum(1 for r in rows if str(r.get(col, "")).strip().lower() in ("1", "true", "yes", "t"))
|
||||
return n / len(rows)
|
||||
|
||||
|
||||
def evaluate(h: Hypothesis, data_dir: str) -> None:
|
||||
if h.id == "H1":
|
||||
rows = _load(os.path.join(data_dir, "signups.csv"))
|
||||
if not rows:
|
||||
return
|
||||
# Require a defined threshold to avoid a fabricated verdict.
|
||||
thr = os.environ.get("H1_THRESHOLD")
|
||||
rate = _rate(rows, "converted_to_foundation")
|
||||
if rate is None:
|
||||
h.evidence = "signups.csv present but column 'converted_to_foundation' empty"
|
||||
return
|
||||
if thr is None:
|
||||
h.verdict = "INSUFFICIENT"
|
||||
h.evidence = f"conversion={rate:.1%} but H1_THRESHOLD undefined; set it to decide KILL/KEEP"
|
||||
return
|
||||
thr_f = float(thr)
|
||||
h.evidence = f"conversion={rate:.1%} (threshold={thr_f:.1%})"
|
||||
h.verdict = "KEEP" if rate >= thr_f else "KILL"
|
||||
elif h.id == "H2":
|
||||
rows = _load(os.path.join(data_dir, "subscribers.csv"))
|
||||
if not rows:
|
||||
return
|
||||
# Compute correlation of practice_signal vs continued, and purchase_recency vs continued.
|
||||
def corr(col):
|
||||
xs, ys = [], []
|
||||
for r in rows:
|
||||
try:
|
||||
xs.append(float(r[col]))
|
||||
ys.append(1.0 if str(r.get("continued", "")).lower() in ("1", "true", "t") else 0.0)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if len(xs) < 3:
|
||||
return None
|
||||
mx, my = sum(xs) / len(xs), sum(ys) / len(ys)
|
||||
num = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
|
||||
den = (sum((x - mx) ** 2 for x in xs) * sum((y - my) ** 2 for y in ys)) ** 0.5
|
||||
return num / den if den else 0.0
|
||||
cp, cr = corr("practice_signal"), corr("purchase_recency_days")
|
||||
if cp is None or cr is None:
|
||||
h.evidence = "subscribers.csv present but insufficient numeric rows"
|
||||
return
|
||||
h.evidence = f"corr(practice,continued)={cp:.2f}; corr(purchase_recency,continued)={cr:.2f}"
|
||||
h.verdict = "KILL" if cr >= cp else "KEEP"
|
||||
elif h.id == "H3":
|
||||
rows = _load(os.path.join(data_dir, "cohort_jan2026.csv"))
|
||||
if not rows:
|
||||
return
|
||||
a = _rate(rows, "bought_ageless")
|
||||
t = _rate(rows, "upgraded_tier2")
|
||||
if a is None or t is None:
|
||||
h.evidence = "cohort_jan2026.csv present but target columns empty"
|
||||
return
|
||||
h.evidence = f"Ageless={a:.1%} (target 25%); Tier2={t:.1%} (target 40%)"
|
||||
h.verdict = "KEEP" if (a >= 0.25 and t >= 0.40) else "REVISE"
|
||||
elif h.id == "H4":
|
||||
rows = _load(os.path.join(data_dir, "capped_clients.csv"))
|
||||
if not rows:
|
||||
return
|
||||
r = _rate(rows, "converted_to_scalable")
|
||||
if r is None:
|
||||
h.evidence = "capped_clients.csv present but column empty"
|
||||
return
|
||||
h.evidence = f"capped->scalable conversion={r:.1%}"
|
||||
h.verdict = "KEEP" if r >= 0.30 else "REVISE"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--data-dir", default=os.path.join(os.path.dirname(__file__), "sample_data"))
|
||||
args = ap.parse_args()
|
||||
print("VEP P0 Falsification Harness — GST reference tenant")
|
||||
print(f"data-dir: {args.data_dir}\n")
|
||||
kills = 0
|
||||
for h in HYPS:
|
||||
evaluate(h, args.data_dir)
|
||||
print(h.report())
|
||||
if h.verdict in ("KILL", "REVISE"):
|
||||
kills += 1
|
||||
print("=" * 60)
|
||||
if kills == 0 and all(h.verdict == "INSUFFICIENT" for h in HYPS):
|
||||
print("GATE: INSUFFICIENT DATA — supply data files to run the gate.")
|
||||
print("This is the honest default: no fabricated verdicts.")
|
||||
else:
|
||||
print(f"GATE: {kills} hypothesis(es) KILL/REVISE. Per P0 acceptance, >=1 kill/revise = gate MET.")
|
||||
print("If KILL/REVISE: prune or revise the build accordingly (the P1 spine is small + reversible).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
20
scripts/p0/sample_data/README.md
Normal file
20
scripts/p0/sample_data/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# VEP P0 — data templates
|
||||
#
|
||||
# Drop real GST exports here as CSV with these exact columns. The harness (run.py)
|
||||
# reads them and emits KILL/KEEP/REVISE. Until files exist (or columns are empty),
|
||||
# the harness reports INSUFFICIENT — it never invents numbers.
|
||||
|
||||
signups.csv columns: email, intent_bucket, converted_to_foundation, date
|
||||
cohort_jan2026.csv columns: attendee, bought_ageless, upgraded_tier2
|
||||
subscribers.csv columns: sub_id, tier, practice_signal, continued, purchase_recency_days
|
||||
capped_clients.csv columns: client_id, revenue_stream, converted_to_scalable
|
||||
|
||||
Where to get these (GST-owned data; not available to the assistant):
|
||||
- signups.csv : export the 190 7-Day Revival signups; tag each with the intent
|
||||
bucket they pick in the H1 test email; mark who bought Foundation.
|
||||
- cohort_jan2026.csv : from the delivered Movement Lab cohort — who bought Ageless / Tier 2.
|
||||
- subscribers.csv : export Tier 1/Tier 2 subscribers + any practice/view signal + churn.
|
||||
- capped_clients.csv : live-class / private-session clients; did they buy video/equipment?
|
||||
|
||||
Run: python scripts/p0/run.py --data-dir scripts/p0/sample_data
|
||||
H1_THRESHOLD=0.10 python scripts/p0/run.py # e.g. need >=10% conversion to build
|
||||
Loading…
Add table
Reference in a new issue