1"""F4-dev-01 replication script. Reads only data/, writes out/results.json deterministically.2Values are extracted by regex from the release text; matched source lines are printed."""3import json, re, hashlib, os45SRC_ID = "statcan-daily-lfs-2026-09"6SRC = f"data/sources/{SRC_ID}.txt"7README = "data/README.md"8EXPECTED_SHA = "7ea5fec57c87874ff5ee09a8304927a1aaed5a1837db5312fab9f7a2d5f2d0bb"910def wc_l(path):11 with open(path, "rb") as f:12 return f.read().count(b"\n")1314raw = open(SRC, "rb").read()15sha = hashlib.sha256(raw).hexdigest()16assert sha == EXPECTED_SHA, f"SHA mismatch: {sha}"17lines = raw.decode("utf-8").split("\n")1819# map each line to the most recent '## ' section heading (level-2 only, as required by TASK.md)20section_of, cur = [], None21for ln in lines:22 if ln.startswith("## "):23 cur = ln[3:].strip()24 section_of.append(cur)2526def find(pattern):27 hits = []28 for i, ln in enumerate(lines):29 m = re.search(pattern, ln)30 if m:31 hits.append((i + 1, section_of[i], m, ln))32 return hits3334def loc(section):35 return f'{SRC_ID} | section "{section}"'3637# (a1) national unemployment rate -- first statement in Highlights38ur_hits = find(r"The unemployment rate (?:increased|rose|fell|decreased) \d+\.\d percentage points to (\d+\.\d)%")39ur_nat = [h for h in ur_hits if h[1] in ("Highlights", "Unemployment rate increases to 6.5%")]40ur_vals = {float(h[2].group(1)) for h in ur_nat}41assert len(ur_vals) == 1, ur_vals42ur = ur_vals.pop()43ur_primary = next(h for h in ur_nat if h[1] == "Highlights")4445# (a2) national employment change46emp_hits = find(r"^(?:In September, e|E)mployment (declined|increased|rose|fell) by ([\d,]+) \(")47emp_vals = set()48for h in emp_hits:49 sign = -1 if h[2].group(1) in ("declined", "fell") else 150 emp_vals.add(sign * int(h[2].group(2).replace(",", "")))51assert len(emp_vals) == 1, emp_vals52emp = emp_vals.pop()53emp_primary = next(h for h in emp_hits if h[1] == "Highlights")5455print("Matched source lines:")56for tag, hs in (("unemployment_rate", ur_nat), ("employment_change", emp_hits)):57 for (n, sec, m, ln) in hs:58 print(f" [{tag}] line {n} | section \"{sec}\": {ln}")5960# (b) assertion: women +0.3 to 5.3%; men ROSE 0.2 to 5.8%61w = find(r"core-aged women \(25 to 54 years old\), the unemployment rate (rose|fell) (\d+\.\d) percentage points to (\d+\.\d)%")62mn = find(r"unemployment rate for core-aged men (rose|fell) (\d+\.\d) percentage points to (\d+\.\d)%")63assert len(w) == 1 and len(mn) == 164wd, wpp, wlvl = w[0][2].groups()65md, mpp, mlvl = mn[0][2].groups()66print(f" [assertion women] line {w[0][0]} | section \"{w[0][1]}\": {w[0][3]}")67print(f" [assertion men] line {mn[0][0]} | section \"{mn[0][1]}\": {mn[0][3]}")68women_ok = (wd == "rose" and wpp == "0.3" and wlvl == "5.3")69men_ok = (md == "rose" and mpp == "0.2" and mlvl == "5.8")70verdict = "supported" if (women_ok and men_ok) else "not_supported"71assert w[0][1] == mn[0][1]72reason = (f"The women's element matches (\"rose {wpp} percentage points to {wlvl}%\"), but the release states that "73 f"\"the unemployment rate for core-aged men {md} {mpp} percentage points to {mlvl}% in the month\", "74 f"contradicting the claim that it rose." if not men_ok else75 "Every element of the assertion is stated in the release.")7677results = {78 "task_id": "F4-dev-01",79 "unemployment_rate_sep2026": ur,80 "employment_change_sep2026_persons": emp,81 "locators": {82 "unemployment_rate_sep2026": loc(ur_primary[1]),83 "employment_change_sep2026_persons": loc(emp_primary[1]),84 },85 "assertion_verdict": verdict,86 "assertion_locator": loc(w[0][1]),87 "assertion_reason": reason,88 "feasibility": "complete",89 "feasibility_reason": None,90 "data_issues": [91 {"issue": "data/README.md rights note refers to re-set tables ('RESEA Tables 2-3, London Table 3 header') and regression-table rendering that do not exist in this extract (apparently template text from another task).",92 "handling": "Ignored; irrelevant to the LFS extract, whose SHA-256 matches the README value. No tables are reproduced in the extract."},93 {"issue": "Some text (e.g. layoff rate, participation rate, population-aging text box) appears under '## Infographic 2' because chart/infographic titles are rendered as '##' headings; the text-box title is not a '##' heading.",94 "handling": "Not relevant to the extracted figures, which appear under substantive narrative headings; locators use the nearest preceding '##' heading."},95 ],96 "n_input_rows": {"README.md": wc_l(README), f"{SRC_ID}.txt": wc_l(SRC)},97 "n_analysed": 3,98 "random_seed": None,99 "baseline_consistency": {100 "unemployment_rate_statements_agree": True,101 "n_unemployment_rate_statements": len(ur_nat),102 "employment_change_statements_agree": True,103 "n_employment_change_statements": len(emp_hits),104 "alternate_locator_unemployment_rate": loc("Unemployment rate increases to 6.5%"),105 "alternate_locator_employment_change": loc("Employment falls for second consecutive month"),106 },107 "source_sha256": sha,108}109os.makedirs("out", exist_ok=True)110with open("out/results.json", "w") as f:111 json.dump(results, f, indent=2, ensure_ascii=False)112 f.write("\n")113print(f"unemployment_rate_sep2026={ur}; employment_change_sep2026_persons={emp}; assertion_verdict={verdict}")114