1"""F4-dev-01: extract headline LFS Sept 2026 figures and assess an assertion.2Reads only data/; writes out/results.json deterministically. No randomness (random_seed = null).3Prints every matched source line so the extraction can be audited."""4import json, os, re56ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))7SRC_ID = "statcan-daily-lfs-2026-09"8SRC = os.path.join(ROOT, "data", "sources", SRC_ID + ".txt")9README = os.path.join(ROOT, "data", "README.md")101112def count_lines(p): # equivalent to `wc -l`: number of newline characters13 with open(p, "rb") as f:14 return f.read().count(b"\n")151617def sectioned_lines(path):18 """Return list of (line_no, current '## ' heading (not '###'), text)."""19 out, sec = [], None20 with open(path, encoding="utf-8") as f:21 for i, line in enumerate(f, 1):22 line = line.rstrip("\n")23 m = re.match(r"^##\s+(?!#)(.*\S)\s*$", line)24 if m:25 sec = m.group(1)26 out.append((i, sec, line))27 return out282930def loc(sec):31 return f'{SRC_ID} | section "{sec}"'323334lines = sectioned_lines(SRC)3536# --- national unemployment rate: sentences "The unemployment rate <verb> [x percentage points] to y%"37ur_pat = re.compile(r"\bThe unemployment rate (increased|rose|fell|decreased|declined)"38 r" (\d+\.\d) percentage points to (\d+\.\d)%")39ur_hits = [(i, sec, m.group(3), t) for i, sec, t in lines for m in ur_pat.finditer(t)]4041# --- national employment change: line-initial "Employment <verb> by N" / "In September, employment <verb> by N"42emp_pat = re.compile(r"^(?:Employment|In September, employment) (declined|fell|decreased|increased|rose) by (\d{1,3}(?:,\d{3})+)")43emp_hits = []44for i, sec, t in lines:45 m = emp_pat.search(t)46 if m and sec is not None:47 emp_hits.append((i, sec, m.group(1), m.group(2), t))4849print("Matched national unemployment-rate lines:")50for h in ur_hits:51 print(f" L{h[0]} [## {h[1]}]: {h[3]}")52print("Matched national employment-change lines:")53for h in emp_hits:54 print(f" L{h[0]} [## {h[1]}]: {h[4]}")5556ur_values = sorted({h[2] for h in ur_hits})57assert len(ur_values) == 1, f"inconsistent national unemployment rates: {ur_values}"58emp_values = sorted({(h[2] in ("declined", "fell", "decreased"), h[3]) for h in emp_hits})59assert len(emp_values) == 1, f"inconsistent employment changes: {emp_values}"6061ur_primary = next(h for h in ur_hits if h[1] == "Highlights")62emp_primary = next(h for h in emp_hits if h[1] == "Highlights")63unemployment_rate = float(ur_primary[2])64neg, num = emp_values[0]65employment_change = (-1 if neg else 1) * int(num.replace(",", ""))6667# --- assertion about core-aged women and men68women_pat = re.compile(r"core-aged women \(25 to 54 years old\), the unemployment rate (rose|fell|increased|decreased) (\d+\.\d) percentage points to (\d+\.\d)%")69men_pat = re.compile(r"the unemployment rate for core-aged men (rose|fell|increased|decreased) (\d+\.\d) percentage points to (\d+\.\d)%")70women = men = None71for i, sec, t in lines:72 m = women_pat.search(t)73 if m:74 women = (i, sec, m.group(1), m.group(2), m.group(3), m.group(0))75 m = men_pat.search(t)76 if m:77 men = (i, sec, m.group(1), m.group(2), m.group(3), m.group(0))78assert women is not None and men is not None, "assertion evidence not found"79print("Assertion evidence:")80print(f" women: L{women[0]} [## {women[1]}]: {women[5]}")81print(f" men: L{men[0]} [## {men[1]}]: {men[5]}")8283elements = {84 "women_direction_rose": women[2] in ("rose", "increased"),85 "women_change_0.3pp": women[3] == "0.3",86 "women_level_5.3pct": women[4] == "5.3",87 "men_direction_rose": men[2] in ("rose", "increased"),88 "men_change_0.2pp": men[3] == "0.2",89 "men_level_5.8pct": men[4] == "5.8",90}91for k, v in elements.items():92 print(f" {k}: {'stated' if v else 'CONTRADICTED'}")93verdict = "supported" if all(elements.values()) else "not_supported"94assertion_locator = " ; ".join(loc(s) for s in sorted({women[1], men[1]}))95if verdict == "not_supported":96 reason = (f"The women's part is stated (\"{women[5]}\"), but the release says \"{men[5]}\", "97 f"which contradicts the asserted rise for core-aged men (the 0.2-point magnitude and 5.8% level match; the direction does not).")98else:99 reason = "All six asserted elements are printed in the release."100101results = {102 "task_id": "F4-dev-01",103 "unemployment_rate_sep2026": unemployment_rate,104 "employment_change_sep2026_persons": employment_change,105 "locators": {106 "unemployment_rate_sep2026": loc(ur_primary[1]),107 "employment_change_sep2026_persons": loc(emp_primary[1]),108 },109 "corroborating_locators": {110 "unemployment_rate_sep2026": sorted({loc(h[1]) for h in ur_hits if h[1] != "Highlights"}),111 "employment_change_sep2026_persons": sorted({loc(h[1]) for h in emp_hits if h[1] != "Highlights"}),112 },113 "n_matches_unemployment_rate": len(ur_hits),114 "n_matches_employment_change": len(emp_hits),115 "assertion_verdict": verdict,116 "assertion_locator": assertion_locator,117 "assertion_reason": reason,118 "assertion_elements": elements,119 "feasibility": "complete",120 "feasibility_reason": None,121 "data_issues": [122 {"issue": "data/README.md ('Rights' line) mentions 'RESEA Tables 2-3, London Table 3 header', and the extract header mentions regression tables; neither exists in this extract (apparent boilerplate shared with other bundles).",123 "handling": "Ignored; it has no bearing on the release text or on the extracted numbers."},124 {"issue": "Lines 53-60 (layoff, job-finding and participation paragraphs and the population-aging text box) have no '## ' heading of their own and fall under '## Infographic 2' in the flattened text; the SDG goal list after 'following goals:' is not reproduced.",125 "handling": "Recorded only; none of the requested figures or assertion sentences are in these parts."},126 ],127 "n_input_rows": {128 "README.md": count_lines(README),129 SRC_ID + ".txt": count_lines(SRC),130 },131 "n_analysed": 3,132 "random_seed": None,133}134with open(os.path.join(ROOT, "out", "results.json"), "w") as f:135 json.dump(results, f, indent=2, sort_keys=True)136 f.write("\n")137print(f"unemployment_rate_sep2026={unemployment_rate}; employment_change_sep2026_persons={employment_change}; verdict={verdict}")138