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 files

The 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>

Files changed (2) hide show
  1. canlex/config.py +69 -2
  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. (The Immigration Appeal Division Rules,
31
- # SOR-2002-230, are intentionally absent: that instrument has been repealed.)
 
 
 
 
 
 
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
- raise ValueError(f"{code}: no <Body> element (root <{root.tag}>)")
 
 
 
 
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