1"""F4-conf-02: verdicts on six assertions vs. cited extracts. Reads data/ only; deterministic (no randomness)."""2import json, math, os, re, hashlib34ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))5D = os.path.join(ROOT, "data")6DCR, RES = "arxiv-2609.27732v1", "arxiv-2606.14621v1"78def read(p):9 with open(os.path.join(D, p), encoding="utf-8") as f:10 return f.read()1112assertions = json.loads(read("assertions.json"))13src = {DCR: read(f"sources/{DCR}.txt"), RES: read(f"sources/{RES}.txt")}14lines = {k: v.splitlines() for k, v in src.items()}1516def line(sid, prefix):17 """Return (lineno, text) of first line starting with prefix; print it; also return following line."""18 for i, l in enumerate(lines[sid]):19 if l.startswith(prefix):20 print(f" [{sid} L{i+1}] {l}")21 nxt = lines[sid][i + 2] if i + 2 < len(lines[sid]) else "" # SE line (blank line between)22 if nxt.startswith("|"):23 print(f" [{sid} L{i+3}] {nxt}")24 return i, l, nxt25 raise RuntimeError(f"line not found: {prefix}")2627def cells(l):28 return [c.strip() for c in l.split("|")]2930def num(c):31 return float(re.sub(r"[^\d.\-]", "", c.replace("−", "-")))3233def pct(b):34 return 100 * (math.exp(b) - 1)3536# ---- baseline: naive verbatim number match (all numbers in assertion occur in cited source) ----37baseline = {}38for a in assertions:39 nums = re.findall(r"\d+\.\d+", a["text"])40 found = [n for n in nums if n in src[a["cites"]]]41 baseline[a["id"]] = "supported" if len(found) == len(nums) else "not_supported"4243V, L, R = {}, {}, {}44COLS = {"1 km": 1, "3 km": 2, "5 km": 3}4546# ---- A1: Table 6, 5 km ----47print("A1 evidence:")48idx6 = next(i for i, l in enumerate(lines[DCR]) if l.startswith("Table 6:"))49sub = lines[DCR][idx6:]50i_att = next(i for i, l in enumerate(sub) if l.startswith("ATT |"))51att, se_l, pct_l = sub[i_att], sub[i_att + 2], sub[i_att + 4]52for l in (att, se_l, pct_l):53 print(" [Table 6]", l)54b5 = num(cells(att)[3]); se5 = num(cells(se_l)[3]); stars5 = cells(att)[3].count("*"); p5 = cells(pct_l)[3]55ok = (b5 == -0.431 and se5 == 0.210 and stars5 >= 2 and p5 == "-35.0%" and abs(pct(b5) - (-35.0)) < 0.05)56V["A1"] = "supported" if ok else "not_supported"57L["A1"] = f'{DCR} | Table 6 | row "ATT" | column "5 km"'58R["A1"] = (f"Table 6 prints ATT {cells(att)[3]} with SE {cells(se_l)[3].strip("()")} (** = p<0.05) and % change {p5}, "59 f"and 100(exp({b5})-1) = {pct(b5):.2f}%, so every element matches.")6061# ---- A2: Table 7 HIV, 1 km ----62print("A2 evidence:")63_, hiv, hiv_se = line(DCR, "HIV |")64b_hiv1 = num(cells(hiv)[1])65V["A2"] = "supported" if b_hiv1 < 0 else "not_supported"66L["A2"] = f'{DCR} | Table 7 | row "HIV" | column "1 km"'67R["A2"] = (f"Table 7 prints the 1 km HIV ATT as +{cells(hiv)[1]} (SE {cells(hiv_se)[1].strip("()")}), positive and insignificant, "68 "so the claimed negative sign and implied fall are contradicted.")6970# ---- A3: Table 6, 3 km ----71print("A3 evidence:")72b3 = num(cells(att)[2]); p3 = cells(pct_l)[2]73V["A3"] = "supported" if abs(pct(b3) - (-28.3)) < 0.05 else "not_supported"74L["A3"] = f'{DCR} | Table 6 | row "% change" | column "3 km"'75R["A3"] = (f"Table 6 converts the 3 km ATT {cells(att)[2]} to {p3} (100(exp({b3})-1) = {pct(b3):.2f}%), "76 "not 28.3%; the assertion reads log points as percent (and the estimate is insignificant, SE "77 f"{cells(se_l)[2].strip("()")}).")7879# ---- A4: Table 7 Hepatitis, 5 km ----80print("A4 evidence:")81_, hep, hep_se = line(DCR, "Hepatitis |")82bh = num(cells(hep)[3]); sh = num(cells(hep_se)[3])83ci196 = (bh - 1.96 * sh, bh + 1.96 * sh); ci2 = (bh - 2 * sh, bh + 2 * sh); ci1 = (bh - sh, bh + sh)84excl = ci196[1] < 0 or ci196[0] > 085V["A4"] = "supported" if (excl and abs(ci196[0] + 0.66) < 0.02 and abs(ci196[1] + 0.16) < 0.02) else "not_supported"86L["A4"] = f'{DCR} | Table 7 | row "Hepatitis" | column "5 km"'87R["A4"] = (f"With ATT {bh} and SE ({sh:.3f}) the 95% CI is [{ci196[0]:.3f}, {ci196[1]:.3f}] (±2SE: "88 f"[{ci2[0]:.3f}, {ci2[1]:.3f}]), which includes zero; the asserted [-0.66, -0.16] is only ±1 SE "89 f"([{ci1[0]:.3f}, {ci1[1]:.3f}]) and the cell carries no significance star.")9091# ---- A5: sex heterogeneity ----92print("A5 evidence: search for sex/gender terms in DCR extract")93hits = [i + 1 for i, l in enumerate(lines[DCR]) if re.search(r"\b(men|women|male|female|sex|gender)\b", l, re.I)]94print(f" lines mentioning men/women/male/female/sex/gender: {hits}")95V["A5"] = "not_assessable" if not hits else "REVIEW"96L["A5"] = f"not reported in {DCR}"97R["A5"] = ("The extract reports ATTs only by buffer (Table 6), outcome category (Table 7) and estimator (Table A19), "98 "never by sex, so the men-vs-women comparison can be neither confirmed nor refuted.")99100# ---- A6: RESEA Table 2, Weeks on UI ----101print("A6 evidence:")102_, ui, _ = line(RES, "Weeks on UI |")103c = cells(ui)104ate = c[4]; ctrl = c[2]105m = re.match(r"(-?[\d.]+) weeks; p<0.001; 95% CI: \[(-?[\d.]+), (-?[\d.]+)\]", ate)106b6, lo6, hi6 = map(float, m.groups()); cm = float(re.match(r"([\d.]+)", ctrl).group(1))107rel = 100 * abs(b6) / cm108V["A6"] = "supported" if (b6 == -1.99 and lo6 == -2.24 and hi6 == -1.75 and cm == 17.3 and round(rel, 1) == 11.5) else "not_supported"109L["A6"] = f'{RES} | Table 2 | row "Weeks on UI" | column "Average treatment effect estimate"'110R["A6"] = (f"Table 2 prints the ATE as {b6} weeks (95% CI [{lo6}, {hi6}]) against a control mean of {cm} weeks, "111 f"and 1.99/17.3 = {rel:.1f}%, matching every element.")112113counts = {k: sum(v == k for v in V.values()) for k in ("supported", "not_supported", "not_assessable")}114115def wcl(p):116 with open(os.path.join(D, p), "rb") as f:117 return f.read().count(b"\n")118119results = {120 "task_id": "F4-conf-02",121 "verdicts": V, "locators": L, "reasons": R,122 "n_supported": counts["supported"], "n_not_supported": counts["not_supported"],123 "n_not_assessable": counts["not_assessable"],124 "feasibility": "complete", "feasibility_reason": None,125 "data_issues": [126 {"issue": f"{DCR}: Table A19 CS-DR column prints SE (0.218) for the ATT -0.431 that Table 6 prints with SE (0.210) (same estimator, buffer and sample).",127 "handling": f"A1 decided on Table 6, which the assertion quotes; under SE 0.218 the estimate remains significant at 5% (|t|={0.431/0.218:.2f} > 1.96) and carries ** in both tables, so the verdict is unaffected."},128 {"issue": f"{DCR}: Table A19 note states 640 never-treated comparison regions whereas Tables 6, 7 and A19 body state 608.",129 "handling": "Recorded only; no assertion depends on the control-region count."},130 {"issue": f"{DCR}: abstract says fatal hospitalisations fall by 35%, but Table 7 5 km fatal ATT -0.354 converts to {pct(-0.354):.1f}% (Section 6.1 says ~30%); abstract appears to read log points as percent.",131 "handling": "Recorded only; verdicts rely on table cells, not the abstract; no assertion concerns fatal outcomes."},132 {"issue": f"{DCR}: results text calls HIV estimates 'close to zero' although the 5 km HIV ATT is -0.340 (SE 0.294).",133 "handling": "Recorded only; A2 decided on the printed 1 km cell (0.063)."},134 {"issue": f"{RES}: text reports CIs for reductions as positive magnitudes ([1.75 weeks, 2.24 weeks]) while Table 2 prints [-2.24, -1.75]; also a rendering defect 'p ¡ 0.001'.",135 "handling": "Treated as sign convention of the prose; A6 decided on Table 2, whose signed interval matches the assertion."},136 ],137 "n_input_rows": {138 "assertions.json": len(assertions),139 "README.md": wcl("README.md"),140 f"{DCR}.txt": wcl(f"sources/{DCR}.txt"),141 f"{RES}.txt": wcl(f"sources/{RES}.txt"),142 },143 "n_analysed": len(V),144 "random_seed": None,145 "baseline_naive_number_match_verdicts": baseline,146 "baseline_agreement_with_final": sum(baseline[k] == V[k] for k in V),147 "derived": {148 "A1_pct_from_logpoints": round(pct(b5), 2),149 "A1_t_stat_table6": round(b5 / se5, 3),150 "A3_pct_from_logpoints": round(pct(b3), 2),151 "A4_ci95_1.96": [round(x, 3) for x in ci196],152 "A4_ci95_2.0": [round(x, 3) for x in ci2],153 "A4_ci_1se": [round(x, 3) for x in ci1],154 "A6_relative_reduction_pct": round(rel, 2),155 },156}157assert "REVIEW" not in V.values()158os.makedirs(os.path.join(ROOT, "out"), exist_ok=True)159with open(os.path.join(ROOT, "out", "results.json"), "w") as f:160 json.dump(results, f, indent=2, ensure_ascii=False)161print("Verdicts:", V)162print("Counts:", counts)163