Beemer Claude Fable 5 commited on
Commit
0b22e9a
·
1 Parent(s): 9fce526

US dispositions: 51-jurisdiction survey, 5-point verdicts, bottom-line decision paths

Browse files

Every disposition entry now carries a real state-by-state breakdown (284 rows
across 50 states + DC): local statute cites, mechanism facts, the state's own
characterization, and an IRPA verdict applied uniformly by rule from the
verified Saini/Burgon/Drake framework. Verdicts moved from yes/no/depends to
a 5-point scale assessed on the COMPLETED disposition (yes / likely-yes /
likely-no / no / fact-specific) after the user rejected 'depends' as
unusable; each entry gained a BOTTOM LINE stating the one or two record facts
that decide the case. The user-review correction on standard of proof is
included: s. 33 reasonable grounds governs (Mugesera para 114), balance of
probabilities only for a PR's s. 36(1)(c) determination (s. 36(3)(d)).

Method documented in-dataset: 10 states agent-researched, 41 extracted from
the Restoration of Rights Project profiles; 9 highest-risk clearance rows
spot-verified verbatim against statute text (zero contradictions); full-
population verification pending and disclosed. commentary.py renders 12
entry chunks (state lists capped, coverage summaries), 51 per-state chunks,
and the methodology. Reviewed by the user 2026-07-15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

canlex/commentary.py CHANGED
@@ -16,6 +16,7 @@ import json
16
  from .config import DATA_DIR, PROCESSED_DIR
17
 
18
  CURATED = DATA_DIR / "curated" / "us_dispositions.json"
 
19
  OUT = PROCESSED_DIR / "commentary.json"
20
 
21
  ACT_CODE = "US-DISP"
@@ -33,22 +34,57 @@ _STATUS_LABEL = {
33
  "no-authority": "NO AUTHORITY LOCATED -- reasoned interpretation only",
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- def _entry_text(e):
38
- """Render one disposition entry as a readable, retrieval-friendly block."""
 
 
 
 
 
 
39
  lines = [BANNER, ""]
40
  lines.append(f"Disposition: {'; '.join(e['names'])}")
41
- lines.append(f"Is it a conviction for IRPA s. 36 purposes: "
42
- f"{e['is_conviction'].upper()}")
43
  lines.append(f"Authority status: {_STATUS_LABEL[e['status']]}")
 
 
 
44
  lines.append("")
45
  lines.append(e["analysis"])
46
  if e.get("state_variations"):
 
 
 
47
  lines.append("")
48
- lines.append("State variations:")
49
- for v in e["state_variations"]:
50
- flag = f" (conviction: {v['is_conviction']})" if v.get("is_conviction") else ""
 
 
 
 
 
 
 
 
51
  lines.append(f"- {v['state']}{flag}: {v['note']}")
 
 
 
52
  if e.get("authorities"):
53
  lines.append("")
54
  lines.append("Authorities:")
@@ -103,20 +139,123 @@ def build():
103
  "part": "US dispositions",
104
  "division": "",
105
  "heading": (f"Is a US {e['names'][0]} a conviction for IRPA "
106
- f"s. 36? ({e['is_conviction']})"),
107
- "text": _entry_text(e),
108
  "history": "",
109
  "last_amended": "",
110
  "current_to": data.get("reviewed", ""),
111
  "citation": f"{ACT_SHORT} — {e['names'][0]}",
112
  "source_url": "",
113
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
115
  OUT.write_text(json.dumps(chunks, ensure_ascii=False, indent=1),
116
  encoding="utf-8")
117
  print(f"{len(chunks)} commentary chunks "
118
  f"({len(data.get('dispositions', []))} dispositions, "
119
- f"{len(data.get('methodology', []))} methodology) -> {OUT}")
 
120
 
121
 
122
  if __name__ == "__main__":
 
16
  from .config import DATA_DIR, PROCESSED_DIR
17
 
18
  CURATED = DATA_DIR / "curated" / "us_dispositions.json"
19
+ EQUIV = DATA_DIR / "curated" / "us_equivalency.json"
20
  OUT = PROCESSED_DIR / "commentary.json"
21
 
22
  ACT_CODE = "US-DISP"
 
34
  "no-authority": "NO AUTHORITY LOCATED -- reasoned interpretation only",
35
  }
36
 
37
+ # Verdicts assess the COMPLETED disposition; the methodology chunk explains
38
+ # the rule each one derives from. 'depends' is the legacy spelling of
39
+ # fact-specific, kept for backwards compatibility.
40
+ _VERDICT_LABEL = {
41
+ "yes": "YES", "likely-yes": "LIKELY YES", "likely-no": "LIKELY NO",
42
+ "no": "NO", "fact-specific": "FACT-SPECIFIC",
43
+ "depends": "FACT-SPECIFIC",
44
+ }
45
+
46
+
47
+ def _vlabel(v):
48
+ return _VERDICT_LABEL.get(v, str(v).upper())
49
 
50
+
51
+ def _entry_text(e, max_states=None):
52
+ """Render one disposition entry as a readable, retrieval-friendly block.
53
+
54
+ With the 51-jurisdiction survey an entry can carry dozens of state rows;
55
+ max_states caps how many render inline (the corpus keeps full per-state
56
+ detail in separate per-state chunks so retrieval still reaches every
57
+ row). None renders everything -- the tool uses that for direct lookups."""
58
  lines = [BANNER, ""]
59
  lines.append(f"Disposition: {'; '.join(e['names'])}")
60
+ lines.append(f"Is the COMPLETED disposition a conviction for IRPA s. 36: "
61
+ f"{_vlabel(e['is_conviction'])}")
62
  lines.append(f"Authority status: {_STATUS_LABEL[e['status']]}")
63
+ if e.get("bottom_line"):
64
+ lines.append("")
65
+ lines.append(f"BOTTOM LINE: {e['bottom_line']}")
66
  lines.append("")
67
  lines.append(e["analysis"])
68
  if e.get("state_variations"):
69
+ named = [v for v in e["state_variations"]
70
+ if v["state"].lower() != "general"]
71
+ shown = named if max_states is None else named[:max_states]
72
  lines.append("")
73
+ if max_states is not None and len(named) > len(shown):
74
+ from collections import Counter
75
+ counts = Counter(v.get("is_conviction", "depends") for v in named)
76
+ lines.append(f"State-by-state coverage: {len(named)} jurisdictions "
77
+ f"({', '.join(f'{k}: {n}' for k, n in counts.most_common())}) "
78
+ f"-- full per-state detail in the per-state entries.")
79
+ else:
80
+ lines.append("State variations:")
81
+ for v in shown:
82
+ flag = (f" (conviction: {_vlabel(v['is_conviction'])})"
83
+ if v.get("is_conviction") else "")
84
  lines.append(f"- {v['state']}{flag}: {v['note']}")
85
+ for v in e["state_variations"]:
86
+ if v["state"].lower() == "general":
87
+ lines.append(f"- General: {v['note']}")
88
  if e.get("authorities"):
89
  lines.append("")
90
  lines.append("Authorities:")
 
139
  "part": "US dispositions",
140
  "division": "",
141
  "heading": (f"Is a US {e['names'][0]} a conviction for IRPA "
142
+ f"s. 36? ({_vlabel(e['is_conviction'])})"),
143
+ "text": _entry_text(e, max_states=6),
144
  "history": "",
145
  "last_amended": "",
146
  "current_to": data.get("reviewed", ""),
147
  "citation": f"{ACT_SHORT} — {e['names'][0]}",
148
  "source_url": "",
149
  })
150
+
151
+ # One chunk per jurisdiction: every disposition row for that state, so a
152
+ # query naming a state ("Georgia first offender act", "Missouri SIS")
153
+ # retrieves that state's page directly.
154
+ by_state = {}
155
+ for e in data.get("dispositions", []):
156
+ for v in e.get("state_variations", []):
157
+ st = v["state"]
158
+ if st.lower() == "general":
159
+ continue
160
+ flag = _vlabel(v.get("is_conviction") or e["is_conviction"])
161
+ by_state.setdefault(st, []).append(
162
+ f"- {e['names'][0]} (conviction: {flag}): {v['note']}")
163
+ for st in sorted(by_state):
164
+ body = (BANNER + "\n\n"
165
+ + f"US dispositions — {st}: whether each disposition type is a "
166
+ f"conviction for IRPA s. 36, under {st} law.\n\n"
167
+ + "\n".join(by_state[st])
168
+ + "\n\nThe act branch (IRPA s. 36(1)(c)/(2)(c)) can apply even "
169
+ "where a disposition is not a conviction. See the "
170
+ "per-disposition entries for the governing analysis and "
171
+ "authorities.")
172
+ slug = st.lower().replace(" ", "-")
173
+ chunks.append({
174
+ "id": f"commentary-state-{slug}",
175
+ "doc_type": "commentary",
176
+ "act_code": ACT_CODE,
177
+ "act_short": ACT_SHORT,
178
+ "act_name": ACT_NAME,
179
+ "section": f"state-{slug}",
180
+ "marginal_note": f"US dispositions — {st}",
181
+ "part": "US dispositions by state",
182
+ "division": "",
183
+ "heading": (f"{st}: criminal dispositions vs the IRPA "
184
+ f"'conviction' concept"),
185
+ "text": body,
186
+ "history": "",
187
+ "last_amended": "",
188
+ "current_to": data.get("reviewed", ""),
189
+ "citation": f"{ACT_SHORT} — {st}",
190
+ "source_url": "",
191
+ })
192
+ # --- equivalency pairings (step 2), same banner discipline
193
+ n_pairings = 0
194
+ if EQUIV.exists():
195
+ eq = json.loads(EQUIV.read_text(encoding="utf-8"))
196
+ for m in eq.get("methodology", []):
197
+ chunks.append({
198
+ "id": f"commentary-method-{m['id']}",
199
+ "doc_type": "commentary",
200
+ "act_code": ACT_CODE,
201
+ "act_short": ACT_SHORT,
202
+ "act_name": ACT_NAME,
203
+ "section": m["id"],
204
+ "marginal_note": m["title"],
205
+ "part": "Methodology",
206
+ "division": "",
207
+ "heading": m["title"],
208
+ "text": BANNER + "\n\n" + m["text"],
209
+ "history": "",
210
+ "last_amended": "",
211
+ "current_to": eq.get("reviewed", ""),
212
+ "citation": f"{ACT_SHORT} — {m['title']}",
213
+ "source_url": "",
214
+ })
215
+ for p in eq.get("pairings", []):
216
+ n_pairings += 1
217
+ lines = [BANNER, "",
218
+ f"US offence: {'; '.join(p['us_terms'][:5])}",
219
+ f"Canadian equivalent: {p['canadian_offence']}",
220
+ f"Maximum penalty (verified): {p['penalty']}",
221
+ f"Inadmissibility branch: {p['branch']}", "",
222
+ p["analysis"]]
223
+ if p.get("caveats"):
224
+ lines.append("")
225
+ lines.append("Caveats:")
226
+ lines += [f"- {c}" for c in p["caveats"]]
227
+ if p.get("authorities"):
228
+ lines.append("")
229
+ lines.append("Authorities:")
230
+ lines += [f"- {a['cite']} ({a['court']}): {a['holding']}"
231
+ for a in p["authorities"]]
232
+ chunks.append({
233
+ "id": f"commentary-equiv-{p['id']}",
234
+ "doc_type": "commentary",
235
+ "act_code": ACT_CODE,
236
+ "act_short": ACT_SHORT,
237
+ "act_name": ACT_NAME,
238
+ "section": f"equiv-{p['id']}",
239
+ "marginal_note": f"Equivalency: {p['us_terms'][0]}",
240
+ "part": "US offence equivalency",
241
+ "division": "",
242
+ "heading": (f"What does a US {p['us_terms'][0]} conviction "
243
+ f"equate to in Canada?"),
244
+ "text": "\n".join(lines),
245
+ "history": "",
246
+ "last_amended": "",
247
+ "current_to": eq.get("reviewed", ""),
248
+ "citation": f"{ACT_SHORT} — equivalency: {p['us_terms'][0]}",
249
+ "source_url": "",
250
+ })
251
+
252
  PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
253
  OUT.write_text(json.dumps(chunks, ensure_ascii=False, indent=1),
254
  encoding="utf-8")
255
  print(f"{len(chunks)} commentary chunks "
256
  f"({len(data.get('dispositions', []))} dispositions, "
257
+ f"{len(by_state)} state pages, {n_pairings} equivalency pairings, "
258
+ f"{len(data.get('methodology', []))}+ methodology) -> {OUT}")
259
 
260
 
261
  if __name__ == "__main__":
data/curated/us_dispositions.json CHANGED
The diff for this file is too large to render. See raw diff
 
tests/test_commentary.py CHANGED
@@ -34,10 +34,17 @@ class EntryTextTests(unittest.TestCase):
34
  def test_carries_banner_and_flags(self):
35
  text = _entry_text(entry())
36
  self.assertTrue(text.startswith(BANNER))
37
- self.assertIn("DEPENDS", text)
 
38
  self.assertIn("NO AUTHORITY LOCATED", text)
39
  self.assertIn("INTERPRETATION (no direct authority", text)
40
 
 
 
 
 
 
 
41
  def test_no_interpretation_block_when_empty(self):
42
  text = _entry_text(entry(interpretation="", status="settled"))
43
  self.assertNotIn("INTERPRETATION", text)
 
34
  def test_carries_banner_and_flags(self):
35
  text = _entry_text(entry())
36
  self.assertTrue(text.startswith(BANNER))
37
+ # legacy 'depends' renders under the 5-point vocabulary
38
+ self.assertIn("FACT-SPECIFIC", text)
39
  self.assertIn("NO AUTHORITY LOCATED", text)
40
  self.assertIn("INTERPRETATION (no direct authority", text)
41
 
42
+ def test_five_point_verdicts_render(self):
43
+ text = _entry_text(entry(is_conviction="likely-no",
44
+ bottom_line="Check completion."))
45
+ self.assertIn("LIKELY NO", text)
46
+ self.assertIn("BOTTOM LINE: Check completion.", text)
47
+
48
  def test_no_interpretation_block_when_empty(self):
49
  text = _entry_text(entry(interpretation="", status="settled"))
50
  self.assertNotIn("INTERPRETATION", text)