| """Ingest CanLex's curated commentary datasets into searchable chunks. |
| |
| Commentary is the one doc_type CanLex AUTHORS rather than mirrors: structured |
| legal-analysis datasets (currently the US-dispositions helper -- whether each |
| kind of US state criminal disposition is a "conviction" for IRPA s. 36 |
| purposes). Every chunk is banner-labelled as commentary, every proposition |
| carries its authorities, and entries with no authority say so explicitly and |
| flag their reasoning as interpretation. The source of truth is |
| data/curated/us_dispositions.json, which is reviewed by the user before it |
| ships; this module just renders it into corpus chunks. |
| |
| py -m canlex.commentary [--allow-shrink] |
| """ |
| import json |
| import sys |
|
|
| from ._common import write_corpus |
| from .config import DATA_DIR, PROCESSED_DIR |
|
|
| CURATED = DATA_DIR / "curated" / "us_dispositions.json" |
| EQUIV = DATA_DIR / "curated" / "us_equivalency.json" |
| OUT = PROCESSED_DIR / "commentary.json" |
|
|
| ACT_CODE = "US-DISP" |
| ACT_SHORT = "US Dispositions Helper" |
| ACT_NAME = ("US criminal dispositions and the IRPA 'conviction' concept " |
| "(curated CanLex commentary)") |
|
|
| BANNER = ("CURATED ANALYSIS -- commentary compiled for CanLex, not a source " |
| "of law. Verify against the cited authorities before relying on it.") |
|
|
| _STATUS_LABEL = { |
| "settled": "Settled by binding authority", |
| "judicially-considered": "Judicially considered (persuasive authority)", |
| "guidance-only": "IRCC guidance only -- no judicial authority located", |
| "no-authority": "NO AUTHORITY LOCATED -- reasoned interpretation only", |
| } |
|
|
| |
| |
| |
| _VERDICT_LABEL = { |
| "yes": "YES", "likely-yes": "LIKELY YES", "likely-no": "LIKELY NO", |
| "no": "NO", "fact-specific": "FACT-SPECIFIC", |
| "depends": "FACT-SPECIFIC", |
| } |
|
|
|
|
| def _vlabel(v): |
| return _VERDICT_LABEL.get(v, str(v).upper()) |
|
|
|
|
| def _entry_text(e, max_states=None): |
| """Render one disposition entry as a readable, retrieval-friendly block. |
| |
| With the 51-jurisdiction survey an entry can carry dozens of state rows; |
| max_states caps how many render inline (the corpus keeps full per-state |
| detail in separate per-state chunks so retrieval still reaches every |
| row). None renders everything -- the tool uses that for direct lookups.""" |
| lines = [BANNER, ""] |
| lines.append(f"Disposition: {'; '.join(e['names'])}") |
| lines.append(f"Is the COMPLETED disposition a conviction for IRPA s. 36: " |
| f"{_vlabel(e['is_conviction'])}") |
| lines.append(f"Authority status: {_STATUS_LABEL[e['status']]}") |
| if e.get("bottom_line"): |
| lines.append("") |
| lines.append(f"BOTTOM LINE: {e['bottom_line']}") |
| lines.append("") |
| lines.append(e["analysis"]) |
| if e.get("state_variations"): |
| named = [v for v in e["state_variations"] |
| if v["state"].lower() != "general"] |
| shown = named if max_states is None else named[:max_states] |
| lines.append("") |
| if max_states is not None and len(named) > len(shown): |
| from collections import Counter |
| counts = Counter(v.get("is_conviction", "depends") for v in named) |
| lines.append(f"State-by-state coverage: {len(named)} jurisdictions " |
| f"({', '.join(f'{k}: {n}' for k, n in counts.most_common())}) " |
| f"-- full per-state detail in the per-state entries.") |
| else: |
| lines.append("State variations:") |
| for v in shown: |
| flag = (f" (conviction: {_vlabel(v['is_conviction'])})" |
| if v.get("is_conviction") else "") |
| lines.append(f"- {v['state']}{flag}: {v['note']}") |
| for v in e["state_variations"]: |
| if v["state"].lower() == "general": |
| lines.append(f"- General: {v['note']}") |
| if e.get("authorities"): |
| lines.append("") |
| lines.append("Authorities:") |
| for a in e["authorities"]: |
| pin = f", {a['pin']}" if a.get("pin") else "" |
| lines.append(f"- {a['cite']} ({a['court']}{pin}): {a['holding']}") |
| if e.get("guidance"): |
| lines.append("") |
| lines.append("IRCC guidance (cited by reference; not reproduced here):") |
| for g in e["guidance"]: |
| lines.append(f"- {g['ref']}: {g['note']}") |
| if e.get("interpretation"): |
| lines.append("") |
| lines.append("INTERPRETATION (no direct authority -- this is CanLex's " |
| "reasoned view from the governing principles; treat it as " |
| "a starting point, not an answer): " |
| + e["interpretation"]) |
| return "\n".join(lines) |
|
|
|
|
| def build(allow_shrink=False): |
| data = json.loads(CURATED.read_text(encoding="utf-8")) |
| chunks = [] |
| for e in data.get("methodology", []): |
| chunks.append({ |
| "id": f"commentary-method-{e['id']}", |
| "doc_type": "commentary", |
| "act_code": ACT_CODE, |
| "act_short": ACT_SHORT, |
| "act_name": ACT_NAME, |
| "section": e["id"], |
| "marginal_note": e["title"], |
| "part": "Methodology", |
| "division": "", |
| "heading": e["title"], |
| "text": BANNER + "\n\n" + e["text"], |
| "history": "", |
| "last_amended": "", |
| "current_to": data.get("reviewed", ""), |
| "citation": f"{ACT_SHORT} — {e['title']}", |
| "source_url": "", |
| }) |
| for e in data.get("dispositions", []): |
| chunks.append({ |
| "id": f"commentary-disp-{e['id']}", |
| "doc_type": "commentary", |
| "act_code": ACT_CODE, |
| "act_short": ACT_SHORT, |
| "act_name": ACT_NAME, |
| "section": e["id"], |
| "marginal_note": e["names"][0], |
| "part": "US dispositions", |
| "division": "", |
| "heading": (f"Is a US {e['names'][0]} a conviction for IRPA " |
| f"s. 36? ({_vlabel(e['is_conviction'])})"), |
| "text": _entry_text(e, max_states=6), |
| "history": "", |
| "last_amended": "", |
| "current_to": data.get("reviewed", ""), |
| "citation": f"{ACT_SHORT} — {e['names'][0]}", |
| "source_url": "", |
| }) |
|
|
| |
| |
| |
| by_state = {} |
| for e in data.get("dispositions", []): |
| for v in e.get("state_variations", []): |
| st = v["state"] |
| if st.lower() == "general": |
| continue |
| flag = _vlabel(v.get("is_conviction") or e["is_conviction"]) |
| by_state.setdefault(st, []).append( |
| f"- {e['names'][0]} (conviction: {flag}): {v['note']}") |
| for st in sorted(by_state): |
| body = (BANNER + "\n\n" |
| + f"US dispositions — {st}: whether each disposition type is a " |
| f"conviction for IRPA s. 36, under {st} law.\n\n" |
| + "\n".join(by_state[st]) |
| + "\n\nThe act branch (IRPA s. 36(1)(c)/(2)(c)) can apply even " |
| "where a disposition is not a conviction. See the " |
| "per-disposition entries for the governing analysis and " |
| "authorities.") |
| slug = st.lower().replace(" ", "-") |
| chunks.append({ |
| "id": f"commentary-state-{slug}", |
| "doc_type": "commentary", |
| "act_code": ACT_CODE, |
| "act_short": ACT_SHORT, |
| "act_name": ACT_NAME, |
| "section": f"state-{slug}", |
| "marginal_note": f"US dispositions — {st}", |
| "part": "US dispositions by state", |
| "division": "", |
| "heading": (f"{st}: criminal dispositions vs the IRPA " |
| f"'conviction' concept"), |
| "text": body, |
| "history": "", |
| "last_amended": "", |
| "current_to": data.get("reviewed", ""), |
| "citation": f"{ACT_SHORT} — {st}", |
| "source_url": "", |
| }) |
| |
| n_pairings = 0 |
| if EQUIV.exists(): |
| eq = json.loads(EQUIV.read_text(encoding="utf-8")) |
| for m in eq.get("methodology", []): |
| chunks.append({ |
| "id": f"commentary-method-{m['id']}", |
| "doc_type": "commentary", |
| "act_code": ACT_CODE, |
| "act_short": ACT_SHORT, |
| "act_name": ACT_NAME, |
| "section": m["id"], |
| "marginal_note": m["title"], |
| "part": "Methodology", |
| "division": "", |
| "heading": m["title"], |
| "text": BANNER + "\n\n" + m["text"], |
| "history": "", |
| "last_amended": "", |
| "current_to": eq.get("reviewed", ""), |
| "citation": f"{ACT_SHORT} — {m['title']}", |
| "source_url": "", |
| }) |
| for p in eq.get("pairings", []): |
| n_pairings += 1 |
| lines = [BANNER, "", |
| f"US offence: {'; '.join(p['us_terms'][:5])}", |
| f"Canadian equivalent: {p['canadian_offence']}", |
| f"Maximum penalty (verified): {p['penalty']}", |
| f"Inadmissibility branch: {p['branch']}", "", |
| p["analysis"]] |
| if p.get("caveats"): |
| lines.append("") |
| lines.append("Caveats:") |
| lines += [f"- {c}" for c in p["caveats"]] |
| if p.get("authorities"): |
| lines.append("") |
| lines.append("Authorities:") |
| lines += [f"- {a['cite']} ({a['court']}): {a['holding']}" |
| for a in p["authorities"]] |
| chunks.append({ |
| "id": f"commentary-equiv-{p['id']}", |
| "doc_type": "commentary", |
| "act_code": ACT_CODE, |
| "act_short": ACT_SHORT, |
| "act_name": ACT_NAME, |
| "section": f"equiv-{p['id']}", |
| "marginal_note": f"Equivalency: {p['us_terms'][0]}", |
| "part": "US offence equivalency", |
| "division": "", |
| "heading": (f"What does a US {p['us_terms'][0]} conviction " |
| f"equate to in Canada?"), |
| "text": "\n".join(lines), |
| "history": "", |
| "last_amended": "", |
| "current_to": eq.get("reviewed", ""), |
| "citation": f"{ACT_SHORT} — equivalency: {p['us_terms'][0]}", |
| "source_url": "", |
| }) |
|
|
| |
| |
| |
| |
| if not write_corpus(OUT, chunks, indent=1, allow_shrink=allow_shrink): |
| return False |
| print(f"{len(chunks)} commentary chunks " |
| f"({len(data.get('dispositions', []))} dispositions, " |
| f"{len(by_state)} state pages, {n_pairings} equivalency pairings, " |
| f"{len(data.get('methodology', []))}+ methodology) -> {OUT}") |
| return True |
|
|
|
|
| def main(): |
| sys.exit(0 if build(allow_shrink="--allow-shrink" in sys.argv) else 1) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|