1"""F4-conf-02: verify six assertions against cited source extracts.2Reads only data/; writes out/results.json deterministically. No randomness."""3import json, math, os, re45ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))6D = os.path.join(ROOT, "data")7DCR, RES = "arxiv-2609.27732v1", "arxiv-2606.14621v1"89def lines(p):10 with open(p, encoding="utf-8") as f:11 return f.read().split("\n")1213assertions = json.load(open(os.path.join(D, "assertions.json"), encoding="utf-8"))14src = {s: lines(os.path.join(D, "sources", s + ".txt")) for s in (DCR, RES)}15srctext = {s: "\n".join(v) for s, v in src.items()}1617def wc_l(p):18 with open(p, "rb") as f:19 return f.read().count(b"\n")2021n_input_rows = {22 "assertions.json": len(assertions),23 "README.md": wc_l(os.path.join(D, "README.md")),24 DCR + ".txt": wc_l(os.path.join(D, "sources", DCR + ".txt")),25 RES + ".txt": wc_l(os.path.join(D, "sources", RES + ".txt")),26}2728def find(s, prefix, after=None):29 """Return (lineno, line) of first line starting with prefix (after a given line index)."""30 start = 0 if after is None else after31 for i in range(start, len(src[s])):32 if src[s][i].startswith(prefix):33 return i, src[s][i]34 raise ValueError(f"{prefix!r} not found in {s}")3536def cells(line):37 return [c.strip() for c in line.split("|")]3839def num(c):40 return float(re.sub(r"[^0-9.\-]", "", c.replace("−", "-")))4142def show(tag, s, i):43 print(f" [{tag}] {s} line {i+1}: {src[s][i]}")4445pct = lambda b: 100.0 * (math.exp(b) - 1.0)46cols = {"1 km": 1, "3 km": 2, "5 km": 3}4748# ---- parse DCR Table 6 ----49t6, _ = find(DCR, "Table 6:")50i_att, l_att = find(DCR, "ATT |", t6)51i_se, l_se = find(DCR, "| (", i_att)52i_pc, l_pc = find(DCR, "% change |", i_att)53# ---- parse DCR Table 7 ----54t7, _ = find(DCR, "Table 7:")55def t7row(name):56 i, l = find(DCR, name + " |", t7)57 j, m = find(DCR, "| (", i)58 return i, l, j, m5960V, L, R, details = {}, {}, {}, {}6162# A1: 5 km overall ATT, SE, 5% significance, 35.0%63print("A1:", assertions[0]["text"])64for i in (i_att, i_se, i_pc): show("Table 6", DCR, i)65att5 = cells(l_att)[cols["5 km"]]; se5 = cells(l_se)[cols["5 km"]]; pc5 = cells(l_pc)[cols["5 km"]]66b, se = num(att5), num(se5)67stars_ok = att5.endswith("**") # ** => p<0.0568ok = (b == -0.431 and se == 0.210 and stars_ok and pc5 == "-35.0%" and abs(pct(b) - (-35.0)) < 0.05)69details["A1"] = {"att": att5, "se": se5, "pct_printed": pc5, "pct_computed": round(pct(b), 2), "z": round(b / se, 3)}70V["A1"] = "supported" if ok else "not_supported"71L["A1"] = f'{DCR} | Table 6 | row "ATT" | column "5 km"'72R["A1"] = ("Table 6 prints the 5 km ATT as -0.431** with SE (0.210) (** = p<0.05) and a % change of -35.0%, "73 "which equals 100(exp(-0.431)-1) = -35.0%, so sign, magnitude, SE, significance and conversion all match.")7475# A2: HIV 1 km negative?76print("A2:", assertions[1]["text"])77i, l, j, m = t7row("HIV"); show("Table 7", DCR, i); show("Table 7", DCR, j)78hiv1 = cells(l)[cols["1 km"]]79details["A2"] = {"att_1km": hiv1, "se_1km": cells(m)[cols["1 km"]]}80V["A2"] = "supported" if num(hiv1) == -0.063 else "not_supported"81L["A2"] = f'{DCR} | Table 7 | row "HIV" | column "1 km"'82R["A2"] = ("Table 7 prints the 1 km HIV ATT as +0.063 (SE 0.801), positive and insignificant, so the assertion's "83 "negative sign (-0.063) and the claimed fall in HIV hospitalisations are contradicted.")8485# A3: 3 km -0.283 => 28.3% reduction?86print("A3:", assertions[2]["text"])87for i in (i_att, i_pc): show("Table 6", DCR, i)88att3 = num(cells(l_att)[cols["3 km"]]); pc3 = cells(l_pc)[cols["3 km"]]89details["A3"] = {"att_3km": att3, "pct_printed": pc3, "pct_computed": round(pct(att3), 2)}90V["A3"] = "supported" if abs(pct(att3) - (-28.3)) < 0.05 else "not_supported"91L["A3"] = f'{DCR} | Table 6 | row "% change" | column "3 km"'92R["A3"] = ("Table 6 converts the 3 km ATT of -0.283 to a -24.7% change (100(exp(-0.283)-1) = -24.6%), not 28.3%, "93 "so the magnitude is wrong (log points were read as percent); the estimate is also insignificant (SE 0.279).")9495# A4: hepatitis 5 km 95% CI approx [-0.66,-0.16], excludes zero?96print("A4:", assertions[3]["text"])97i, l, j, m = t7row("Hepatitis"); show("Table 7", DCR, i); show("Table 7", DCR, j)98hb, hse = num(cells(l)[cols["5 km"]]), num(cells(m)[cols["5 km"]])99ci196 = [round(hb - 1.96 * hse, 3), round(hb + 1.96 * hse, 3)]100ci200 = [round(hb - 2.0 * hse, 3), round(hb + 2.0 * hse, 3)]101ci1se = [round(hb - hse, 3), round(hb + hse, 3)]102details["A4"] = {"att_5km": hb, "se_5km": hse, "ci95_1.96": ci196, "ci95_2.00": ci200, "pm_1se": ci1se,103 "stars": "none"}104excl = ci196[1] < 0 or ci196[0] > 0105V["A4"] = "supported" if (excl and abs(ci196[0] + 0.66) < 0.02 and abs(ci196[1] + 0.16) < 0.02) else "not_supported"106L["A4"] = f'{DCR} | Table 7 | row "Hepatitis" | column "5 km"'107R["A4"] = (f"Table 7 prints the 5 km hepatitis ATT as -0.406 with SE (0.250) and no star, giving a 95% CI of "108 f"[{ci196[0]}, {ci196[1]}] (±1.96 SE), which includes zero; the asserted [-0.66, -0.16] is only ±1 SE.")109110# A5: men vs women -- search for sex/gender breakdown111print("A5:", assertions[4]["text"])112pat = re.compile(r"\b(men|women|male|female|sex|gender)\b", re.I)113hits = [(k + 1, ln) for k, ln in enumerate(src[DCR]) if pat.search(ln)]114print(f" sex/gender terms found in {DCR}: {len(hits)}", hits)115details["A5"] = {"sex_term_hits": len(hits)}116V["A5"] = "not_assessable" if not hits else "REVIEW"117L["A5"] = f"not reported in {DCR}"118R["A5"] = ("The extract reports ATTs only by buffer, diagnostic category and estimator (Tables 6, 7, A19) and contains "119 "no estimate by sex, so the men-versus-women comparison can be neither confirmed nor refuted.")120121# A6: RESEA weeks on UI122print("A6:", assertions[5]["text"])123k, row = find(RES, "Weeks on UI |"); show("Table 2", RES, k)124kt, _ = find(RES, "Overall, we find"); print(f" [Section 3 text] {RES} line {kt+1}: (contains '1.99 week reduction' and '11.5% decrease ... 17.3 weeks')")125c = cells(row)126ctrl = num(c[2].split("weeks")[0]); ate = c[4]127ate_val = float(re.search(r"(-?\d+\.\d+) weeks", ate).group(1))128ci = [float(x) for x in re.search(r"\[(-?\d+\.\d+), (-?\d+\.\d+)\]", ate).groups()]129rel = 100 * abs(ate_val) / ctrl130details["A6"] = {"ate": ate_val, "ci": ci, "control_mean": ctrl, "pct_of_control": round(rel, 2),131 "text_has_11.5": "11.5% decrease" in src[RES][kt]}132ok = ate_val == -1.99 and ci == [-2.24, -1.75] and ctrl == 17.3 and abs(rel - 11.5) < 0.05133V["A6"] = "supported" if ok else "not_supported"134L["A6"] = f'{RES} | Table 2 | row "Weeks on UI" | column "Average treatment effect estimate"'135R["A6"] = ("Table 2 prints the ATE on weeks on UI as -1.99 weeks (95% CI [-2.24, -1.75], p<0.001) with control mean 17.3 weeks, "136 "and 1.99/17.3 = 11.5%, matching the Section 3 text.")137138# ---- naive baseline: 'supported' iff every number in the assertion appears literally in the cited extract ----139def nums_in(t):140 return re.findall(r"\d+\.\d+|\d+", t.replace("−", "-"))141baseline = {}142for a in assertions:143 ns = [n for n in nums_in(a["text"]) if n not in ("1", "3", "5", "95")]144 baseline[a["id"]] = "supported" if all(n in srctext[a["cites"]] for n in ns) else "not_supported"145146data_issues = [147 {"issue": "DCR Table A19 prints SE (0.218) for the CS-DR 5 km ATT -0.431, while Table 6 prints (0.210) for the same estimate; the A19 note says 640 never-treated regions while A19's body and Tables 6-7 say 608.",148 "handling": "A1 decided on Table 6 (the cited overall-ATT table); the 5% significance claim holds under either SE (z=-2.05 or -1.98), so the verdict is unaffected; flagged."},149 {"issue": "DCR abstract states fatal hospitalisations fall by 35% and narcotics rise 'by a comparable amount', but Table 7 (5 km: -0.354, +0.390) implies -29.8% and +47.7%, and Section 6.1 says ~30% and 47%.",150 "handling": "Not decisive for any assertion; tables used as the authoritative values; flagged."},151 {"issue": "DCR text describes HIV estimates as 'close to zero' although the 3 km and 5 km cells are -0.239 and -0.340 (insignificant).",152 "handling": "A2 decided on the printed sign of the 1 km Table 7 cell (+0.063), not on the prose."},153 {"issue": "DCR Figure 7 note refers to four diagnostic sub-categories whereas Table 7 and the text list five.",154 "handling": "Not decisive; noted only."},155 {"issue": "RESEA results text prints CIs of reductions as positive magnitudes ([1.75, 2.24]) while Table 2 prints signed bounds [-2.24, -1.75]; the text also contains a rendering defect 'p ¡ 0.001' for p < 0.001.",156 "handling": "Treated as equivalent representations; A6 decided on Table 2 signed values."},157]158159counts = {v: sum(1 for x in V.values() if x == v) for v in ("supported", "not_supported", "not_assessable")}160results = {161 "task_id": "F4-conf-02",162 "verdicts": V, "locators": L, "reasons": R,163 "n_supported": counts["supported"], "n_not_supported": counts["not_supported"],164 "n_not_assessable": counts["not_assessable"],165 "feasibility": "complete", "feasibility_reason": None,166 "data_issues": data_issues,167 "n_input_rows": n_input_rows,168 "n_analysed": len(V),169 "random_seed": None,170 "baseline_verdicts_literal_number_match": baseline,171 "computation_details": details,172}173assert sum(counts.values()) == 6 and "REVIEW" not in V.values()174os.makedirs(os.path.join(ROOT, "out"), exist_ok=True)175with open(os.path.join(ROOT, "out", "results.json"), "w", encoding="utf-8") as f:176 json.dump(results, f, indent=2, ensure_ascii=False, sort_keys=True)177print("\nVerdicts:", V)178print("Counts:", counts)179print("Baseline (literal number match):", baseline)180