diff --git a/BUILD_LOG.md b/BUILD_LOG.md index 6ec1c23..0339a90 100644 --- a/BUILD_LOG.md +++ b/BUILD_LOG.md @@ -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 diff --git a/compose.yml b/compose.yml index 53cfa07..6a7f4fd 100644 --- a/compose.yml +++ b/compose.yml @@ -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 diff --git a/scripts/p0/run.py b/scripts/p0/run.py new file mode 100644 index 0000000..7e11ba8 --- /dev/null +++ b/scripts/p0/run.py @@ -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()) diff --git a/scripts/p0/sample_data/README.md b/scripts/p0/sample_data/README.md new file mode 100644 index 0000000..a48587d --- /dev/null +++ b/scripts/p0/sample_data/README.md @@ -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