US dispositions: 51-jurisdiction survey, 5-point verdicts, bottom-line decision paths
Browse filesEvery 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 +149 -10
- data/curated/us_dispositions.json +0 -0
- tests/test_commentary.py +8 -1
|
@@ -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 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
lines = [BANNER, ""]
|
| 40 |
lines.append(f"Disposition: {'; '.join(e['names'])}")
|
| 41 |
-
lines.append(f"Is
|
| 42 |
-
f"{e['is_conviction']
|
| 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 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
| 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__":
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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 |
-
|
|
|
|
| 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)
|