Beemer Claude Fable 5 commited on
Commit ·
37c10f4
1
Parent(s): ac9119b
Parse statutory Schedules; add 10 instruments incl. firearms classification and IAD Rules 2022
Browse filesThe XML parser silently dropped every Schedule except NifProvs, so the CDSA
drug schedules, the AMPS designated-provisions table and IRPA's Refugee
Convention were absent. Schedule-only instruments (Import Control List) now
parse too; the Customs Tariff opts out (tariff_schedule.py serves its
nomenclature). New sources: SOR/98-462, Excise Act 2001, ICL, ECL,
Presentation of Persons Regs, Duty Free Shop Regs, Sufferance Warehouses
Regs, CHRA, GECA, IAD Rules 2022 (the old config comment wrongly treated the
IAD rules as repealed-and-gone).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- canlex/config.py +69 -2
- canlex/ingest.py +115 -1
canlex/config.py
CHANGED
|
@@ -27,8 +27,14 @@ SOURCES = {
|
|
| 27 |
"short": "Citizenship Act", "name": "Citizenship Act", "kind": "act",
|
| 28 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/C-29.xml",
|
| 29 |
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/c-29/"},
|
| 30 |
-
# IRB tribunal procedural rules. (
|
| 31 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
"SOR-2002-229": {
|
| 33 |
"short": "Immigration Division Rules", "name": "Immigration Division Rules",
|
| 34 |
"kind": "regulation",
|
|
@@ -50,10 +56,47 @@ SOURCES = {
|
|
| 50 |
"short": "Customs Act", "name": "Customs Act", "kind": "act",
|
| 51 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/C-52.6.xml",
|
| 52 |
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/c-52.6/"},
|
|
|
|
|
|
|
|
|
|
| 53 |
"C-54.011": {
|
| 54 |
"short": "Customs Tariff", "name": "Customs Tariff", "kind": "act",
|
|
|
|
| 55 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/C-54.011.xml",
|
| 56 |
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/c-54.011/"},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
"S-8.35": {
|
| 58 |
"short": "Luxury Tax Act", "name": "Select Luxury Items Tax Act", "kind": "act",
|
| 59 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/S-8.35.xml",
|
|
@@ -113,6 +156,18 @@ SOURCES = {
|
|
| 113 |
"short": "Firearms Act", "name": "Firearms Act", "kind": "act",
|
| 114 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/F-11.6.xml",
|
| 115 |
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/f-11.6/"},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
"P-24.501": {
|
| 117 |
"short": "PCMLTFA",
|
| 118 |
"name": "Proceeds of Crime (Money Laundering) and Terrorist Financing Act",
|
|
@@ -237,6 +292,18 @@ SOURCES = {
|
|
| 237 |
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/SOR-96-263/"},
|
| 238 |
|
| 239 |
# --- Labour & public service ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
"L-2": {
|
| 241 |
"short": "Canada Labour Code", "name": "Canada Labour Code", "kind": "act",
|
| 242 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/L-2.xml",
|
|
|
|
| 27 |
"short": "Citizenship Act", "name": "Citizenship Act", "kind": "act",
|
| 28 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/C-29.xml",
|
| 29 |
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/c-29/"},
|
| 30 |
+
# IRB tribunal procedural rules. (SOR-2002-230 was repealed because the
|
| 31 |
+
# Immigration Appeal Division Rules, 2022 -- SOR-2022-277, below --
|
| 32 |
+
# replaced it in January 2023.)
|
| 33 |
+
"SOR-2022-277": {
|
| 34 |
+
"short": "IAD Rules", "name": "Immigration Appeal Division Rules, 2022",
|
| 35 |
+
"kind": "regulation",
|
| 36 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/SOR-2022-277.xml",
|
| 37 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/SOR-2022-277/"},
|
| 38 |
"SOR-2002-229": {
|
| 39 |
"short": "Immigration Division Rules", "name": "Immigration Division Rules",
|
| 40 |
"kind": "regulation",
|
|
|
|
| 56 |
"short": "Customs Act", "name": "Customs Act", "kind": "act",
|
| 57 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/C-52.6.xml",
|
| 58 |
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/c-52.6/"},
|
| 59 |
+
# skip_schedules: the Customs Tariff's schedule is the entire tariff
|
| 60 |
+
# nomenclature -- tariff_schedule.py serves chapters 98/99 from the CBSA
|
| 61 |
+
# edition instead, so the XML schedule would only duplicate it at scale.
|
| 62 |
"C-54.011": {
|
| 63 |
"short": "Customs Tariff", "name": "Customs Tariff", "kind": "act",
|
| 64 |
+
"skip_schedules": True,
|
| 65 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/C-54.011.xml",
|
| 66 |
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/c-54.011/"},
|
| 67 |
+
# Traveller/trade instruments under the Customs Act and EIPA. The two
|
| 68 |
+
# control lists are almost entirely Schedules, so they depend on the
|
| 69 |
+
# schedule parser; Presentation of Persons is the legal basis for NEXUS
|
| 70 |
+
# and every other alternative-presentation program.
|
| 71 |
+
"E-14.1": {
|
| 72 |
+
"short": "Excise Act, 2001", "name": "Excise Act, 2001", "kind": "act",
|
| 73 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/E-14.1.xml",
|
| 74 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/e-14.1/"},
|
| 75 |
+
"CRC-604": {
|
| 76 |
+
"short": "Import Control List", "name": "Import Control List",
|
| 77 |
+
"kind": "regulation",
|
| 78 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/C.R.C.,_c._604.xml",
|
| 79 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/C.R.C.,_c._604/"},
|
| 80 |
+
"SOR-89-202": {
|
| 81 |
+
"short": "Export Control List", "name": "Export Control List",
|
| 82 |
+
"kind": "regulation",
|
| 83 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/SOR-89-202.xml",
|
| 84 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/SOR-89-202/"},
|
| 85 |
+
"SOR-2003-323": {
|
| 86 |
+
"short": "Presentation of Persons Regs",
|
| 87 |
+
"name": "Presentation of Persons (2003) Regulations", "kind": "regulation",
|
| 88 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/SOR-2003-323.xml",
|
| 89 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/SOR-2003-323/"},
|
| 90 |
+
"SOR-86-1072": {
|
| 91 |
+
"short": "Duty Free Shop Regs", "name": "Duty Free Shop Regulations",
|
| 92 |
+
"kind": "regulation",
|
| 93 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/SOR-86-1072.xml",
|
| 94 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/SOR-86-1072/"},
|
| 95 |
+
"SOR-86-1065": {
|
| 96 |
+
"short": "Sufferance Warehouses Regs",
|
| 97 |
+
"name": "Customs Sufferance Warehouses Regulations", "kind": "regulation",
|
| 98 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/SOR-86-1065.xml",
|
| 99 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/SOR-86-1065/"},
|
| 100 |
"S-8.35": {
|
| 101 |
"short": "Luxury Tax Act", "name": "Select Luxury Items Tax Act", "kind": "act",
|
| 102 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/S-8.35.xml",
|
|
|
|
| 156 |
"short": "Firearms Act", "name": "Firearms Act", "kind": "act",
|
| 157 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/F-11.6.xml",
|
| 158 |
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/f-11.6/"},
|
| 159 |
+
# Made under Criminal Code s. 117.15: the model-by-model prohibited/
|
| 160 |
+
# restricted/non-restricted classifications (incl. the May 2020
|
| 161 |
+
# SOR/2020-96 prohibitions). Nearly all substance is in its Schedules --
|
| 162 |
+
# this entry is why the schedule parser exists.
|
| 163 |
+
"SOR-98-462": {
|
| 164 |
+
"short": "Firearms Classification Regs",
|
| 165 |
+
"name": ("Regulations Prescribing Certain Firearms and Other Weapons, "
|
| 166 |
+
"Components and Parts of Weapons, Accessories, Cartridge "
|
| 167 |
+
"Magazines, Ammunition and Projectiles as Prohibited, "
|
| 168 |
+
"Restricted or Non-Restricted"), "kind": "regulation",
|
| 169 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/SOR-98-462.xml",
|
| 170 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/SOR-98-462/"},
|
| 171 |
"P-24.501": {
|
| 172 |
"short": "PCMLTFA",
|
| 173 |
"name": "Proceeds of Crime (Money Laundering) and Terrorist Financing Act",
|
|
|
|
| 292 |
"web_base": "https://laws-lois.justice.gc.ca/eng/regulations/SOR-96-263/"},
|
| 293 |
|
| 294 |
# --- Labour & public service ---
|
| 295 |
+
# CHRA grounds the accommodation/discrimination case law already in the
|
| 296 |
+
# corpus (Renaud, Hydro-Québec, Elk Valley); GECA governs injury-on-duty
|
| 297 |
+
# compensation interlocking with the FB agreement's IOD leave.
|
| 298 |
+
"H-6": {
|
| 299 |
+
"short": "CHRA", "name": "Canadian Human Rights Act", "kind": "act",
|
| 300 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/H-6.xml",
|
| 301 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/h-6/"},
|
| 302 |
+
"G-5": {
|
| 303 |
+
"short": "GECA", "name": "Government Employees Compensation Act",
|
| 304 |
+
"kind": "act",
|
| 305 |
+
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/G-5.xml",
|
| 306 |
+
"web_base": "https://laws-lois.justice.gc.ca/eng/acts/g-5/"},
|
| 307 |
"L-2": {
|
| 308 |
"short": "Canada Labour Code", "name": "Canada Labour Code", "kind": "act",
|
| 309 |
"xml_url": "https://laws-lois.justice.gc.ca/eng/XML/L-2.xml",
|
canlex/ingest.py
CHANGED
|
@@ -91,6 +91,115 @@ def _render_block(el, is_section=False):
|
|
| 91 |
return head
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def _history(section):
|
| 95 |
note = section.find("HistoricalNote")
|
| 96 |
if note is None:
|
|
@@ -107,7 +216,11 @@ def parse_legislation(xml_path, code):
|
|
| 107 |
current_to = root.get(f"{LIMS}current-date", "")
|
| 108 |
body = root.find("Body")
|
| 109 |
if body is None:
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
headings, chunks = {}, []
|
| 113 |
for el in body:
|
|
@@ -204,6 +317,7 @@ def parse_legislation(xml_path, code):
|
|
| 204 |
chunks.append({**base, "id": f"{chunk_id}-p{k}", "text": piece,
|
| 205 |
"citation": f"{citation} (part {k} of {n})"})
|
| 206 |
|
|
|
|
| 207 |
chunks.extend(_nif_chunks(root, code, src, current_to))
|
| 208 |
return chunks
|
| 209 |
|
|
|
|
| 91 |
return head
|
| 92 |
|
| 93 |
|
| 94 |
+
def _table_text(tablegroup):
|
| 95 |
+
"""Render a CALS <TableGroup> as caption/title lines plus one
|
| 96 |
+
pipe-separated line per row. Column structure survives as ' | '
|
| 97 |
+
separators, which is enough for BM25/semantic matching and readable in a
|
| 98 |
+
result pane; no attempt is made to preserve spanned cells."""
|
| 99 |
+
out = []
|
| 100 |
+
for t in (tablegroup.find("Caption"), tablegroup.find("table/title")):
|
| 101 |
+
if t is not None:
|
| 102 |
+
txt = _itertext(t)
|
| 103 |
+
if txt:
|
| 104 |
+
out.append(txt)
|
| 105 |
+
for row in tablegroup.iter("row"):
|
| 106 |
+
cells = [_itertext(e) for e in row.findall("entry")]
|
| 107 |
+
line = " | ".join(cells).strip(" |")
|
| 108 |
+
if line:
|
| 109 |
+
out.append(line)
|
| 110 |
+
return "\n".join(out)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _sched_body(el):
|
| 114 |
+
"""Render a <Schedule>'s content elements into readable text.
|
| 115 |
+
|
| 116 |
+
Observed shapes across the corpus: List/Item trees (CDSA I-IV, IRPR
|
| 117 |
+
1.1), DocumentInternal/Group/Provision prose (CDSA V-VI, IRPA's Refugee
|
| 118 |
+
Convention), and TableGroup grids (IRPR Schedule 2 violations, the AMPS
|
| 119 |
+
designated-provisions tables). Lists, groups and provisions reuse
|
| 120 |
+
_render_block, whose Label/Text recursion matches their structure;
|
| 121 |
+
tables get the dedicated renderer above so rows keep their shape."""
|
| 122 |
+
parts = []
|
| 123 |
+
for child in el:
|
| 124 |
+
tag = child.tag
|
| 125 |
+
if tag in ("ScheduleFormHeading", "HistoricalNote"):
|
| 126 |
+
continue
|
| 127 |
+
if tag == "TableGroup":
|
| 128 |
+
parts.append(_table_text(child))
|
| 129 |
+
elif tag == "GroupHeading":
|
| 130 |
+
parts.append(_heading_text(child))
|
| 131 |
+
elif tag in ("DocumentInternal", "Group", "RegulationPiece", "BillPiece"):
|
| 132 |
+
parts.append(_sched_body(child))
|
| 133 |
+
else:
|
| 134 |
+
rendered = _render_block(child)
|
| 135 |
+
if rendered:
|
| 136 |
+
parts.append(rendered)
|
| 137 |
+
return "\n".join(p for p in parts if p)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# Schedule ids with dedicated handling: NifProvs is ingested separately as
|
| 141 |
+
# not-in-force chunks; RelatedProvs (transitional provisions of past
|
| 142 |
+
# amendments) is deliberately excluded -- see _nif_chunks.
|
| 143 |
+
_SCHED_SKIP_IDS = {"NifProvs", "RelatedProvs"}
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _schedule_chunks(root, code, src, current_to):
|
| 147 |
+
"""Chunks for an Act's substantive Schedules.
|
| 148 |
+
|
| 149 |
+
Previously every Schedule except NifProvs was silently dropped -- which
|
| 150 |
+
meant the CDSA's controlled-substance lists, the AMPS designated-
|
| 151 |
+
provisions tables and IRPA's Refugee Convention articles were absent
|
| 152 |
+
from the corpus even though the sections that depend on them were
|
| 153 |
+
ingested. A source can opt out with 'skip_schedules': True (the Customs
|
| 154 |
+
Tariff does: its schedule is the full tariff nomenclature, served
|
| 155 |
+
separately by tariff_schedule.py from the CBSA edition)."""
|
| 156 |
+
if src.get("skip_schedules"):
|
| 157 |
+
return []
|
| 158 |
+
chunks = []
|
| 159 |
+
for n, sched in enumerate(root.findall("Schedule"), start=1):
|
| 160 |
+
if sched.get("id") in _SCHED_SKIP_IDS:
|
| 161 |
+
continue
|
| 162 |
+
h = sched.find("ScheduleFormHeading")
|
| 163 |
+
label = _norm(h.findtext("Label") or "") if h is not None else ""
|
| 164 |
+
title = _norm(h.findtext("TitleText") or "") if h is not None else ""
|
| 165 |
+
oref = _norm(h.findtext("OriginatingRef") or "") if h is not None else ""
|
| 166 |
+
label = label or (f"SCHEDULE ({n})" if not title else title)
|
| 167 |
+
repealed = sched.find("Repealed") is not None
|
| 168 |
+
body = "[Repealed]" if repealed else _sched_body(sched)
|
| 169 |
+
if not body:
|
| 170 |
+
continue
|
| 171 |
+
header = " — ".join(p for p in (title, oref) if p)
|
| 172 |
+
text = (header + "\n" if header else "") + body
|
| 173 |
+
base = {
|
| 174 |
+
"act_code": code,
|
| 175 |
+
"act_short": src["short"],
|
| 176 |
+
"act_name": src["name"],
|
| 177 |
+
"section": label.title(),
|
| 178 |
+
"marginal_note": title or label.title(),
|
| 179 |
+
"part": "SCHEDULES",
|
| 180 |
+
"division": "",
|
| 181 |
+
"heading": label.title(),
|
| 182 |
+
"history": "",
|
| 183 |
+
"last_amended": sched.get(f"{LIMS}lastAmendedDate", ""),
|
| 184 |
+
"in_force": sched.get(f"{LIMS}inforce-start-date", ""),
|
| 185 |
+
"status": "repealed" if repealed else "in force",
|
| 186 |
+
"current_to": current_to,
|
| 187 |
+
"citation": f"{src['short']}, {label.title()}",
|
| 188 |
+
"source_url": src["web_base"],
|
| 189 |
+
}
|
| 190 |
+
pieces = (_split_text(text)
|
| 191 |
+
if len(text) > _SPLIT_THRESHOLD else [text])
|
| 192 |
+
total = len(pieces)
|
| 193 |
+
for k, piece in enumerate(pieces, start=1):
|
| 194 |
+
if total == 1:
|
| 195 |
+
chunks.append({**base, "id": f"{code}-sched{n}", "text": piece})
|
| 196 |
+
else:
|
| 197 |
+
chunks.append({
|
| 198 |
+
**base, "id": f"{code}-sched{n}-p{k}", "text": piece,
|
| 199 |
+
"citation": f"{base['citation']} (part {k} of {total})"})
|
| 200 |
+
return chunks
|
| 201 |
+
|
| 202 |
+
|
| 203 |
def _history(section):
|
| 204 |
note = section.find("HistoricalNote")
|
| 205 |
if note is None:
|
|
|
|
| 216 |
current_to = root.get(f"{LIMS}current-date", "")
|
| 217 |
body = root.find("Body")
|
| 218 |
if body is None:
|
| 219 |
+
# A schedule-only instrument (e.g. the Import Control List) has no
|
| 220 |
+
# <Body> at all -- its entire substance is in Schedules.
|
| 221 |
+
if not root.findall("Schedule"):
|
| 222 |
+
raise ValueError(f"{code}: no <Body> element (root <{root.tag}>)")
|
| 223 |
+
body = ()
|
| 224 |
|
| 225 |
headings, chunks = {}, []
|
| 226 |
for el in body:
|
|
|
|
| 317 |
chunks.append({**base, "id": f"{chunk_id}-p{k}", "text": piece,
|
| 318 |
"citation": f"{citation} (part {k} of {n})"})
|
| 319 |
|
| 320 |
+
chunks.extend(_schedule_chunks(root, code, src, current_to))
|
| 321 |
chunks.extend(_nif_chunks(root, code, src, current_to))
|
| 322 |
return chunks
|
| 323 |
|