1"""F4-dev-01: extract headline LFS figures and assess an assertion from the StatCan Daily extract.2Reads only data/; writes out/results.json deterministically. No randomness."""3import json, re, os, hashlib45ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))6SRC_ID = "statcan-daily-lfs-2026-09"7SRC = os.path.join(ROOT, "data", "sources", SRC_ID + ".txt")8README = os.path.join(ROOT, "data", "README.md")91011def wc_l(p):12 with open(p, "rb") as f:13 return f.read().count(b"\n")141516lines = open(SRC, encoding="utf-8").read().split("\n")17# map each line to the most recent '## ' section heading (not '###')18sections, cur = [], None19for ln in lines:20 m = re.match(r"^##\s+(.*\S)\s*$", ln)21 if m and not ln.startswith("###"):22 cur = m.group(1)23 sections.append(cur)242526def find(pattern, section=None):27 return [(i, lines[i], sections[i]) for i in range(len(lines))28 if re.search(pattern, lines[i]) and (section is None or sections[i] == section)]293031def loc(sec):32 return f'{SRC_ID} | section "{sec}"'333435# (a1) unemployment rate, Highlights36pat_u = r"The unemployment rate (?:increased|rose|fell|decreased|was little changed) [0-9.]+ percentage points to (\d+\.\d)%"37hits_u = find(pat_u, "Highlights")38assert hits_u, "unemployment rate not found in Highlights"39i, txt, sec = hits_u[0]40unemp = float(re.search(pat_u, txt).group(1))41print(f"[unemployment] line {i+1} ({sec}): {txt}")42SEC_U2 = "Unemployment rate increases to 6.5%"43h2 = find(r"The unemployment rate rose [\d.]+ percentage points to (\d+\.\d)% in September", SEC_U2)44assert h245unemp2 = float(re.search(r"to (\d+\.\d)% in September", h2[0][1]).group(1))46print(f" cross-check line {h2[0][0]+1} ({SEC_U2}): {h2[0][1][:100]}")4748# (a2) employment change, Highlights49pat_e = r"Employment (declined|increased|rose|fell) by ([\d,]+) \(([-+]?\d+\.\d)%\) in September"50hits_e = find(pat_e, "Highlights")51assert hits_e, "employment change not found in Highlights"52i, txt, sec = hits_e[0]53m = re.search(pat_e, txt)54sign = -1 if m.group(1) in ("declined", "fell") else 155emp = sign * int(m.group(2).replace(",", ""))56print(f"[employment] line {i+1} ({sec}): {txt}")57SEC_E2 = "Employment falls for second consecutive month"58e2 = find(r"employment (declined|fell) by ([\d,]+) \(", SEC_E2)59assert e260m2 = re.search(r"employment (declined|fell) by ([\d,]+) \(", e2[0][1])61emp2 = -int(m2.group(2).replace(",", ""))62print(f" cross-check line {e2[0][0]+1} ({SEC_E2}): {e2[0][1][:100]}")6364# (b) assertion65SEC_B = "Unemployment rate up for core-aged women"66w = find(r"core-aged women \(25 to 54 years old\), the unemployment rate (rose|fell) (\d+\.\d) percentage points to (\d+\.\d)%", SEC_B)67mm = find(r"unemployment rate for core-aged men (rose|fell) (\d+\.\d) percentage points to (\d+\.\d)%", SEC_B)68assert w and mm69wd = re.search(r"unemployment rate (rose|fell) (\d+\.\d) percentage points to (\d+\.\d)%", w[0][1]).groups()70md = re.search(r"core-aged men (rose|fell) (\d+\.\d) percentage points to (\d+\.\d)%", mm[0][1]).groups()71print(f"[assertion women] line {w[0][0]+1} ({SEC_B}): {w[0][1][:160]}")72print(f"[assertion men] line {mm[0][0]+1} ({SEC_B}): {mm[0][1]}")73women_ok = tuple(wd) == ("rose", "0.3", "5.3")74men_ok = tuple(md) == ("rose", "0.2", "5.8")75if women_ok and men_ok:76 verdict, reason = "supported", "The release states both elements as asserted."77else:78 verdict = "not_supported"79 reason = (f"The women's element matches (\"the unemployment rate {wd[0]} {wd[1]} percentage points to {wd[2]}%\") "80 f"but the release states \"the unemployment rate for core-aged men {md[0]} {md[1]} percentage points to {md[2]}%\", "81 f"contradicting the claim that the men's rate rose.")82print(f"[verdict] {verdict}: women_match={women_ok}, men_match={men_ok}")8384results = {85 "task_id": "F4-dev-01",86 "unemployment_rate_sep2026": unemp,87 "employment_change_sep2026_persons": emp,88 "locators": {89 "unemployment_rate_sep2026": loc(hits_u[0][2]),90 "employment_change_sep2026_persons": loc(hits_e[0][2]),91 },92 "assertion_verdict": verdict,93 "assertion_locator": loc(SEC_B),94 "assertion_reason": reason,95 "feasibility": "complete",96 "feasibility_reason": None,97 "data_issues": [98 {"issue": "data/README.md mentions re-set tables ('RESEA Tables 2-3, London Table 3 header') and regression-table conventions that do not correspond to anything in this extract (apparently shared template text).",99 "handling": "Ignored as irrelevant to this extract, which contains no tables; extract SHA-256 verified against README."},100 {"issue": "Data tables and charts are not reproduced in the extract (headings only).",101 "handling": "All values taken from the narrative text; no table values were needed."},102 {"issue": "Note-to-readers sections use '###' headings rather than '##'.",103 "handling": "Only '##' headings are used as locators; no extracted figure lies under a '###' heading."},104 ],105 "n_input_rows": {"README.md": wc_l(README), SRC_ID + ".txt": wc_l(SRC)},106 "n_analysed": 3,107 "random_seed": None,108 "source_sha256": hashlib.sha256(open(SRC, "rb").read()).hexdigest(),109 "baseline_unemployment_rate_dedicated_section": unemp2,110 "baseline_employment_change_dedicated_section": emp2,111 "headline_crosscheck_agree": bool(unemp == unemp2 and emp == emp2),112}113with open(os.path.join(ROOT, "out", "results.json"), "w") as f:114 json.dump(results, f, indent=2, sort_keys=True)115print("wrote out/results.json")116