"""F4-dev-01: extract headline LFS Sept 2026 figures and assess an assertion. Reads only data/; writes out/results.json deterministically. No randomness (random_seed = null). Prints every matched source line so the extraction can be audited.""" import json, os, re ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SRC_ID = "statcan-daily-lfs-2026-09" SRC = os.path.join(ROOT, "data", "sources", SRC_ID + ".txt") README = os.path.join(ROOT, "data", "README.md") def count_lines(p): # equivalent to `wc -l`: number of newline characters with open(p, "rb") as f: return f.read().count(b"\n") def sectioned_lines(path): """Return list of (line_no, current '## ' heading (not '###'), text).""" out, sec = [], None with open(path, encoding="utf-8") as f: for i, line in enumerate(f, 1): line = line.rstrip("\n") m = re.match(r"^##\s+(?!#)(.*\S)\s*$", line) if m: sec = m.group(1) out.append((i, sec, line)) return out def loc(sec): return f'{SRC_ID} | section "{sec}"' lines = sectioned_lines(SRC) # --- national unemployment rate: sentences "The unemployment rate [x percentage points] to y%" ur_pat = re.compile(r"\bThe unemployment rate (increased|rose|fell|decreased|declined)" r" (\d+\.\d) percentage points to (\d+\.\d)%") ur_hits = [(i, sec, m.group(3), t) for i, sec, t in lines for m in ur_pat.finditer(t)] # --- national employment change: line-initial "Employment by N" / "In September, employment by N" emp_pat = re.compile(r"^(?:Employment|In September, employment) (declined|fell|decreased|increased|rose) by (\d{1,3}(?:,\d{3})+)") emp_hits = [] for i, sec, t in lines: m = emp_pat.search(t) if m and sec is not None: emp_hits.append((i, sec, m.group(1), m.group(2), t)) print("Matched national unemployment-rate lines:") for h in ur_hits: print(f" L{h[0]} [## {h[1]}]: {h[3]}") print("Matched national employment-change lines:") for h in emp_hits: print(f" L{h[0]} [## {h[1]}]: {h[4]}") ur_values = sorted({h[2] for h in ur_hits}) assert len(ur_values) == 1, f"inconsistent national unemployment rates: {ur_values}" emp_values = sorted({(h[2] in ("declined", "fell", "decreased"), h[3]) for h in emp_hits}) assert len(emp_values) == 1, f"inconsistent employment changes: {emp_values}" ur_primary = next(h for h in ur_hits if h[1] == "Highlights") emp_primary = next(h for h in emp_hits if h[1] == "Highlights") unemployment_rate = float(ur_primary[2]) neg, num = emp_values[0] employment_change = (-1 if neg else 1) * int(num.replace(",", "")) # --- assertion about core-aged women and men women_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)%") men_pat = re.compile(r"the unemployment rate for core-aged men (rose|fell|increased|decreased) (\d+\.\d) percentage points to (\d+\.\d)%") women = men = None for i, sec, t in lines: m = women_pat.search(t) if m: women = (i, sec, m.group(1), m.group(2), m.group(3), m.group(0)) m = men_pat.search(t) if m: men = (i, sec, m.group(1), m.group(2), m.group(3), m.group(0)) assert women is not None and men is not None, "assertion evidence not found" print("Assertion evidence:") print(f" women: L{women[0]} [## {women[1]}]: {women[5]}") print(f" men: L{men[0]} [## {men[1]}]: {men[5]}") elements = { "women_direction_rose": women[2] in ("rose", "increased"), "women_change_0.3pp": women[3] == "0.3", "women_level_5.3pct": women[4] == "5.3", "men_direction_rose": men[2] in ("rose", "increased"), "men_change_0.2pp": men[3] == "0.2", "men_level_5.8pct": men[4] == "5.8", } for k, v in elements.items(): print(f" {k}: {'stated' if v else 'CONTRADICTED'}") verdict = "supported" if all(elements.values()) else "not_supported" assertion_locator = " ; ".join(loc(s) for s in sorted({women[1], men[1]})) if verdict == "not_supported": reason = (f"The women's part is stated (\"{women[5]}\"), but the release says \"{men[5]}\", " f"which contradicts the asserted rise for core-aged men (the 0.2-point magnitude and 5.8% level match; the direction does not).") else: reason = "All six asserted elements are printed in the release." results = { "task_id": "F4-dev-01", "unemployment_rate_sep2026": unemployment_rate, "employment_change_sep2026_persons": employment_change, "locators": { "unemployment_rate_sep2026": loc(ur_primary[1]), "employment_change_sep2026_persons": loc(emp_primary[1]), }, "corroborating_locators": { "unemployment_rate_sep2026": sorted({loc(h[1]) for h in ur_hits if h[1] != "Highlights"}), "employment_change_sep2026_persons": sorted({loc(h[1]) for h in emp_hits if h[1] != "Highlights"}), }, "n_matches_unemployment_rate": len(ur_hits), "n_matches_employment_change": len(emp_hits), "assertion_verdict": verdict, "assertion_locator": assertion_locator, "assertion_reason": reason, "assertion_elements": elements, "feasibility": "complete", "feasibility_reason": None, "data_issues": [ {"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).", "handling": "Ignored; it has no bearing on the release text or on the extracted numbers."}, {"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.", "handling": "Recorded only; none of the requested figures or assertion sentences are in these parts."}, ], "n_input_rows": { "README.md": count_lines(README), SRC_ID + ".txt": count_lines(SRC), }, "n_analysed": 3, "random_seed": None, } with open(os.path.join(ROOT, "out", "results.json"), "w") as f: json.dump(results, f, indent=2, sort_keys=True) f.write("\n") print(f"unemployment_rate_sep2026={unemployment_rate}; employment_change_sep2026_persons={employment_change}; verdict={verdict}")