Code, design and documents 9 files
docs/DATA-MAINTENANCE.md +10 −4
@@ -705,4 +705,5 @@ segments:
S$5,000", "and 3–6 strokes of the cane". `penalties.summary` records whether a ladder reaches
caning or death. The audit (below) fails on minimums or caning written only in a note.
+- **Life imprisonment:** `life: mandatory` for a required life sentence; `life: possible` for an alternative, combined with `prison_min`, `prison` or `death` where the law provides those alternatives. Never substitute a finite maximum for life. API rows preserve this field, and summaries include `life` and `smallPrisonLife`.
- **Prison or a fine.** When the law gives alternatives ("at least three years or a fine of at
least S$5,000"), fill both minimums and say in `note`: "Prison or a fine, or both" (or "Prison
@@ -717,7 +718,8 @@ segments:
(`over:` equals the previous `upto:`), treatments get no milder as amounts grow, and one ladder
counts either grams or plants, never both.
-- **Presumptions.** Where an amount is presumed to be for trafficking (Singapore above 15 g),
- give the trafficking punishment in the step, call it "Presumed trafficking" in `term`, and say
- in `note` what applies if the presumption is rebutted.
+- **Presumptions.** State the exact product and the rebuttable trigger: Singapore uses more than
+ 15 g of plant material, 10 g of resin and 30 g of mixture. Keep actual possession sentences
+ separate from trafficking sentences where a combined ladder would hide either outcome. A
+ presumption is not a conviction; label the offence and explain what happens if it is rebutted.
- **Criminal fines without a fixed maximum** (day fines, as in Germany) are left out of `fine`;
say "a criminal fine instead of prison is possible" in `note`.
@@ -2015,5 +2017,9 @@ left untouched. Today's scores are unchanged.
Use `recreational: death` wherever an in-force law permits death for a cannabis offence,
-including a qualifying repeat offence, even if another preparation has a lawful route.
+including a qualifying repeat offence or a drug-supply offence requiring a death caused by the
+supplied narcotic, even if another preparation has a lawful route. Guyana’s section 6 requires
+supply or administration to someone under eighteen and proof that the narcotic caused their death;
+the category records the statutory possibility, without implying an ordinary possession or
+trafficking death sentence or a verified cannabis execution.
This status takes precedence on the law map and in the homepage count. Explain the
lawful preparation separately in the summary, segments and quick answers. India’s bhang
docs/research/bug-review-2026-10-28.md +128 −0
@@ -75,2 +75,130 @@ JSON were checked for Iceland, Pakistan, Armenia, Moldova and Bosnia; the remove
Pakistan claims are absent. CSV exports carry Iceland strict/hemp, Moldova standard/hemp and
Bosnia strict/hemp, and the new Iceland history page exists.
+
+## Second pass: penalty tables and hemp classifications (#276–#292)
+
+Reviewed sixteen newly filed reports (there is no #280 in this set). Left #252 and #253 with
+ their existing reviewers: their comments already record the remaining Mali/Armenia legal questions.
+No personal-use score changed. Guyana's statutory maximum classification changes to `death`, under
+ the existing rule that includes narrowly conditioned capital offences; this does not establish
+ a cannabis sentence or execution, or change ordinary-user enforcement scores.
+
+| Issue | Profile | Resolution |
+|---|---|---|
+| #276 | China | Existing capital qualification was already partly corrected. Split ordinary small and serious small trafficking, specify all three product thresholds, cite Article 347 directly, and encode the life alternative. |
+| #277 | Iran | Separate ordinary former-capital/former-life cases from Article 45 cases. First-degree imprisonment replaces former death; second-degree replaces former life. The over-50-kg clause expressly names Article 4; do not automatically apply it to every Article 5 possession offence. |
+| #278 | Brunei | Separate plant material/resin for trafficking, import/export and possession for trafficking. Resin trafficking/import uses 130–200 g and >200 g; possession for trafficking uses 200–300 g and >300 g. |
+| #279 | Singapore | Actual possession sentences now have product-specific rows; trafficking sentences are in sharing. Explain rebuttable triggers >15 g plant material, >10 g resin, >30 g mixture, including visitors and the presumption spotlight. |
+| #281 | Iraq | Weighing discrepancy is a criminal fine, with repeat-offence imprisonment and/or a higher fine; it is not a civil possession allowance. |
+| #282 | Libya | **Partial; leave open.** Remove current paid/unpaid supply rows until Law 19/1996's replacement of Article 35 penalties is reconciled with the later amendment. Explain the original ten-year minimum as historical text, not a verified current maximum. |
+| #283 | Sudan | Supply minimum is `prison_min: 10 years`; preserve the separate aggravated capital/life row. Remove the prohibited enforcement penalty table while retaining its court example in prose. |
+| #284 | Malaysia/shared | Add validated `life: mandatory/possible`, preserve it in parsed/API rows and summaries, use the shared formatter in comparison, and encode Malaysia's cultivation, heavier possession and trafficking sentences. |
+| #285 | Ghana | Hemp is standard (≤0.3%), and the documented low-THC production route is hemp. The regulator lists licensed processing/trade as well as growing; this is not evidence of a separate higher-THC cannabis industry. |
+| #286 | Oman | Hemp is none: processed-fibre/sterilised-seed exceptions and institutional medical/scientific licences do not create a general hemp crop threshold. |
+| #287 | Philippines | The checkout already had hemp none. Re-read RA 9165, add the statutory locator and regression protection. |
+| #288 | Taiwan | The checkout already had hemp none. Re-read cultivation law and FDA notice, add the finished-cosmetic locator and regression protection. |
+| #289 | Colombia | Hemp is high: ministry explicitly says <1% remains applicable pending the separate Law 2204/2022 implementing rules. Cultivation remains licensed. |
+| #290 | Guyana | Hemp-only production; add standard hemp and qualify cultivation/seed prose. The enacted law includes **exactly** 0.3% delta-9 THC on a dry-weight basis. May 2026 implementation report is not proof of commercial harvests. |
+| #291 | Guyana | Add the section 6 fatal-minor-supply caveat, separate it from ordinary section 5 life imprisonment, and use the existing maximum-penalty classification. Read the 2022 amendment and 2023 CCJ judgment; neither repeals section 6. No cannabis-specific sentence/execution was verified. |
+| #292 | Suriname | Enacted 2023 licensed hemp ≤1%, hemp-only production, certified seed, and implementation qualification. The law is dated 13 September, **published 14 September and effective 15 September 2023**. It raises a prior 0.3% exception, not a first-ever 2022 legalisation. |
+
+### Sources read in this pass and limits
+
+- China: [Criminal Law, Articles 347–348, official English text](https://en.spp.gov.cn/2020-12/26/c_948417_13.htm)
+ and [SPC Interpretation 2016 No. 8, Articles 1–2](https://www.court.gov.cn/fabu/xiangqing/19122.html).
+ The capital tier includes specified conduct as well as large quantities.
+- Iran: [final adopted Article 45, Boroumand Center translation](https://www.iranrights.org/library/document/3262),
+ [UNODC translation of the older drug law reproduced by the Center](https://www.iranrights.org/library/document/4019),
+ and [Penal Code Article 19, IHRDC translation](https://iranhrdc.org/wp-content/uploads/pdf_en/Iranian_Codes/Translation_of_the_new_Penal_Code_Book_1and2__507475863.pdf).
+ The older law is not independently current after 2017. Article 45(D) names Article 4 for the
+ traditional-drug weight condition. Do not manufacture an Article 5 weight-only capital rule.
+ The table leaves variable/current monetary penalties unquantified rather than inventing them.
+- Brunei: [AGC current Chapter 27 listing](https://www.agc.gov.bn/documents/misuse-of-drugs-act/?agc_sl=7&agc_tab=sl)
+ and its [2013 revised Act, Second Schedule](https://www.agc.gov.bn/wp-content/uploads/2026/07/CAP-27-2013-ED-MISUSE-OF-DRUGS-ACT.pdf),
+ including printed pp. 47–50 and 53–54. The old `/AGC Images/` PDF URL was unavailable; the
+ current AGC listing supplies the replacement. The possession-for-trafficking offence has
+ different thresholds from actual trafficking/import.
+- Singapore: [section 17, official indexed current text](https://sso.agc.gov.sg/Act/MDA1973?ProvIds=pr17-&ValidDate=20260601)
+ and [CNB product-specific penalty tables, updated 2 July 2026](https://www.cnb.gov.sg/singapore-drug-situation/drugs--psychoactive-substances--and-inhalants/cannabis/).
+ Direct section-page requests returned 403; the official search index returned the full
+ section 17(d)–(f) passage and rebuttal wording. CNB tables were readable directly.
+- Malaysia: [Ministry of Health Act 234 incorporating Act 846](https://pharmacy.moh.gov.my/sites/default/files/document-upload/dangerous-drugs-act-1952-act-234_2.pdf),
+ sections 6B(3), 39A and 39B. Downloaded/extracted after repeated web-reader timeouts;
+ section 6B explicitly requires life and at least six strokes.
+- Iraq: [Article 33, original Arabic text in UNODC SHERLOC](https://sherloc.unodc.org/cld/ar/legislation/irq/law_no._50_of_2017_on_narcotics_and_psychotropic_substances/chapter_8/article_33/article_33.html),
+ especially 33(2)(a)–(b). The criminal fine concerns weighing differences, not a general
+ personal-use gram threshold.
+- Sudan: [1994 Act, Arabic Articles 15–20 in SHERLOC](https://sherloc.unodc.org/cld/en/legislation/sdn/_1994_/_/_15-20/law_on_drug_offences_of_1994.html),
+ Article 16(1) clearly says not less than ten years; Article 16(2) lists aggravations.
+ A comprehensive subsequent amendment chronology remains outside this correction.
+- Libya: [1990 law, Article 35, statutory reproduction](https://lawsociety.ly/legislation/قانون-رقم-7-لسنة-1990-م-بشأن-المخدرات-والمؤث/),
+ [Law 19 of 1996, Articles 1–3](https://lawsociety.ly/legislation/قانون-رقم-19-لسنة-1996-م-بشأن-تعديل-بعض-أحكام/),
+ and [later Law 23/2002, Arabic text in DCAF](https://security-legislation.ly/latest-laws/law-no-23-of-2002-amending-and-adding-certain-provisions-to-law-no-7-of-1990-on-narcotics-and-psychotropic-substances/).
+ The official Ministry of Justice URL failed initially; its indexed text corroborated the
+ original Article 35. The 1996 law expressly replaces Articles 34, 35 and 44 penalties with
+ Penal Code Article 169 penalties, without requiring trafficking intent. The later law
+ replaces Article 34 and adds Article 35 aggravations but does not expressly reproduce the
+ original Article 35 basic term. **Current ordinary Article 35 sentencing remains unresolved.**
+- Ghana: [NCC Cannabis Regulations](https://www.ncc.gov.gh/cannabis-regulations/), downloaded/read
+ after web-reader follow-ups timed out. It repeatedly confines licensed cannabis to ≤0.3% THC
+ and lists cultivation, processing and trade. Preserve patient-access status and scores.
+- Oman: [Royal Decree 67/2026, Articles 3 and 7 and schedules](https://qanoon.om/p/2026/rd2026067/).
+ The low-THC crop exception is absent; public institutional research permissions are separate.
+- Philippines: [RA 9165, Supreme Court E-Library](https://elibrary.judiciary.gov.ph/thebookshelf/showdocs/2/1435),
+ sections 3(v) and 16. No general low-THC crop exception; medical laboratory permissions are
+ not an industrial-hemp crop threshold.
+- Taiwan: [Narcotics Hazard Prevention Act, Articles 2 and 12](https://law.moj.gov.tw/ENG/LawClass/LawAll.aspx?pcode=C0000008)
+ and [FDA notice of 19 January 2026](https://www.fda.gov.tw/tc/csmnewsContent.aspx?id=7574&mid=271).
+ The latter discusses finished cosmetic residues of 10 ppm and does not authorise growing.
+- Colombia: [Justice Ministry's cannabis/hemp programme page](https://minjusticia.gov.co/programas-co/Cannabis-con-fines-medicinales-cientificos-industriales/Paginas/default.aspx),
+ “¿Qué es cáñamo?” expressly distinguishes the operative <1% rule from the separate 0.3%
+ legislation awaiting implementation. Checked for later implementing rules; none superseding
+ this crop classification was verified. The direct Decree 811 portal returned 502.
+- Guyana: [Industrial Hemp Act 13/2022, actual Gazette](https://parliament.gov.gy/documents/acts/23476-industrial_hemp_act_2022_-_no._13_of_2022.pdf),
+ visually read printed pp. 206–208, 216 and 234–235; sections 2, 12, 39. `/new2/` downloads
+ failed, but the current `/documents/acts/` path returned the scanned Act.
+ [May 2026 agriculture update](https://agriculture.gov.gy/2026/05/21/regions-six-ten-earmarked-for-industrial-hemp-cultivation-as-govt-pursues-processing-plant-mustapha/)
+ says the authority is operational and large-scale cultivation awaits processing capacity.
+ [Cap. 10:10, sections 3, 5–6 and First Schedule](https://mola.gov.gy/laws/Volume%204%20Cap.%2010.08%20-%2012.211696971083.pdf),
+ [CANU current explanation](https://canu.gov.gy/guyana-drug-laws/),
+ [2022 amending Act](https://parliament.gov.gy/documents/acts/23504-act_no._22_of_2022.pdf),
+ [2023 CCJ Harte/Greenidge judgment](https://www.dpp.gy/wp-content/uploads/2016/08/CCJs-Harte-Greenidge.pdf),
+ and [WCADP, updated 27 October 2026](https://worldcoalition.org/pays/guyana/).
+ CANU mislabels the small-possession amendment as a 2021 bill, so it is used only for its
+ section 6 summary. The CCJ declined an academic constitutional challenge; it did not strike
+ down the death penalty. The 2022 Act changes possession/related provisions, not section 6.
+ No cannabis-specific prosecution, sentence or execution under section 6 was established.
+- Suriname: [enacted S.B. 2023 no. 137](https://www.sris.sr/wp-content/uploads/2023/10/S.B.-2023-no.-137-Wet-Industriele-Hennepteelt.pdf),
+ downloaded and visually read printed pp. 2–3, 21–23 because text extraction was empty.
+ Definition is **Article 1(d)**, not the issue's Article 2(d). Article 29 and the publication
+ imprint establish 15 September commencement. The explanatory memorandum records the earlier
+ 0.3% exception; the 2023 Act raises it to 1%.
+ [Parliament passage notice](https://www.dna.sr/nieuws/parlement-neemt-wetswijziging-industriele-hennepteelt-aan/)
+ and [November 2024 implementation update](https://gov.sr/voorbereidingen-implementatie-wet-industriele-hennepteelt-in-volle-gang/)
+ distinguish the 2022 proposal, 2023 enactment and unfinished implementation. No licensed
+ commercial harvest or completed implementation was verified.
+
+### Shared life-sentence audit and validation
+
+Searched all country penalty notes for life sentences without structured custody fields. The new
+field fixes Malaysia's reported row and the source-verified alternatives in this pass. Many other
+profiles still retain life only in notes; a new audit warning identifies those for source-by-source
+migration. They were **not** converted mechanically: some concern old law, alternatives or repeat
+cases. The API retains `life`, and summaries expose `life`/`smallPrisonLife` (the finite maximum is null for life); comparison uses the
+same punishment formatter as country tables.
+
+An unrelated concurrent Afghanistan commit placed new segment narratives under `scores` and left
+old narratives under `segments`. Commit 5ffc5e8 restores its existing numeric scores, moves the new
+content to its intended segment fields, and expresses compound durations as 30/18 months. This is
+a structural repair of that commit, not an independent verification of its legal research.
+
+Validation: all 193 tests pass (including ten new regression tests); selected-page build passes;
+full build passes (5,946 pages); SEO audit passes (5,948 HTML files, 5,731 sitemap entries and
+275,914 internal links). Inspected all fifteen affected country pages, the Malaysia/Singapore/Guyana
+penalty exports and the hemp/production CSV categories. Profile audits: zero errors, fifteen
+warnings (including existing content gaps and life-only notes awaiting source review).
+
+Final checks ran on an isolated snapshot of committed base `2c762370` plus this batch, with Git
+history available and the repository’s immutable caches reused. Concurrent uncommitted country
+edits and the shared history rewrite were excluded; only the three authored history entries will
+be committed from this batch. No personal-use score changed.
scripts/profile-audit.cjs +3 −2
@@ -56,6 +56,6 @@ for (const id of ids) {
prevRank = Math.max(prevRank, RANK[r.treatment]);
}
- if ((r.treatment === "legal" || r.treatment === "tolerated") && (r.prison || r.prison_min || r.fine || r.fine_min || r.caning || r.death)) err(id, at, `${r.treatment} with a punishment; a punishment means civil or criminal`);
- if (r.treatment === "criminal" && !r.prison && !r.prison_min && !r.fine && !r.fine_min && !r.death && !r.note) warn(id, at, "criminal step with no punishment and no note saying why");
+ if ((r.treatment === "legal" || r.treatment === "tolerated") && (r.prison || r.prison_min || r.fine || r.fine_min || r.caning || r.death || r.life)) err(id, at, `${r.treatment} with a punishment; a punishment means civil or criminal`);
+ if (r.treatment === "criminal" && !r.prison && !r.prison_min && !r.fine && !r.fine_min && !r.death && !r.life && !r.note) warn(id, at, "criminal step with no punishment and no note saying why");
if ((r.treatment === "criminal" || r.treatment === "civil") && !r.term) warn(id, at, "no term: give the law's own word for the offence");
if (r.prison_min && r.prison && days(r.prison_min) > days(r.prison)) err(id, at, `prison_min ${r.prison_min} is longer than prison ${r.prison}`);
@@ -63,4 +63,5 @@ for (const id of ids) {
// facts that have a field of their own but were left in the note
const note = String(r.note ?? "");
+ if (/\blife imprisonment\b|\bimprisonment for life\b/i.test(note) && !r.life) warn(id, at, "the note mentions life imprisonment; verify the source and encode life: mandatory or possible");
// a minimum prison term or a minimum fine written in the note instead of its field
if (/\b(at least|minimum of|not less than)\s+(\w+\s+){0,2}(day|week|month|year)s?\b/i.test(note) && !r.prison_min) err(id, at, `the note gives a minimum prison term ("${note.slice(0, 60)}"); use prison_min`);
scripts/profile-claims.test.mjs +109 −0
@@ -971,2 +971,111 @@ test('Armenian scientific exceptions survive the List No. 1 description (#253)',
assert.match(am.context.medical.text, /dronabinol/);
});
+
+test('life imprisonment survives parsing, display and JSON export (#284)', async () => {
+ const { punishmentText } = await import('../src/lib/penalties.js');
+ const grow = country('my').segments.cultivation.penalties[0];
+ const parsed = JSON.parse(JSON.stringify(penaltyRow(grow)));
+ assert.equal(parsed.life, 'mandatory');
+ assert.equal(punishmentText(parsed), 'Life imprisonment');
+ assert.deepEqual(parsed.caning, [6, 24]);
+ assert.equal(penaltySummary([parsed]).smallPrisonLife, true);
+ assert.equal(penaltySummary([parsed]).smallPrisonDays, null);
+ assert.equal(punishmentText(penaltyRow({ label: 'Alternative', treatment: 'criminal', prison_min: '5 years', life: 'possible' })), '5 years to life imprisonment');
+ assert.equal(punishmentText(penaltyRow({ label: 'Capital alternative', treatment: 'criminal', death: true, life: 'possible' })), 'Death or life imprisonment');
+ const comparison = fs.readFileSync(path.join(root, 'src/pages/compare.astro'), 'utf8');
+ assert.match(comparison, /import \{ punishmentText \}/);
+});
+
+test('China capital trafficking stays qualified by quantity or aggravation (#276)', () => {
+ const rows = country('cn').segments.sharing.penalties;
+ assert.equal(rows.find((r) => /without serious/.test(r.label)).prison, '3 years');
+ assert.ok(rows.some((r) => /30–<150 kg leaf, 2–<10 kg resin or 1–<5 kg oil/.test(r.label) && !r.death));
+ assert.ok(rows.filter((r) => r.death).every((r) => /150 kg.*10 kg.*5 kg.*aggravation/.test(r.label)));
+ assert.equal(rows.find((r) => r.death).prison, '15 years');
+});
+
+test('Iran ordinary weight bands do not unconditionally prescribe death (#277)', () => {
+ const c = country('ir');
+ for (const seg of ['possession', 'sharing']) {
+ const rows = c.segments[seg].penalties;
+ assert.ok(rows.filter((r) => r.death).every((r) => /Article 45/.test(r.label)));
+ assert.ok(rows.filter((r) => /without.*Article 45/.test(r.label)).every((r) => !r.death && r.prison));
+ }
+ assert.equal(c.segments.sharing.penalties.find((r) => /over 5–50 kg/.test(r.label)).prison, '30 years');
+ assert.equal(c.segments.possession.penalties.find((r) => /more than 100 kg/.test(r.label)).prison, '25 years');
+ assert.match(c.segments.possession.text, /over-50-kg.*expressly concerns Article 4/);
+});
+
+test('Brunei keeps plant material and resin thresholds separate by offence (#278)', () => {
+ const rows = country('bn').segments.sharing.penalties;
+ for (const offence of ['Trafficking', 'Import or export']) {
+ assert.ok(rows.some((r) => r.label === `${offence}, resin: more than 200 g` && r.death));
+ const medium = rows.find((r) => r.label === `${offence}, resin: 130–200 g`);
+ assert.equal(medium.prison_min, '20 years');
+ assert.equal(medium.prison, '30 years');
+ assert.deepEqual(medium.caning, [15, 15]);
+ assert.ok(rows.some((r) => r.label === `${offence}, plant material: more than 500 g` && r.death));
+ }
+ assert.ok(rows.some((r) => r.label === 'Possession for trafficking, resin: 200–300 g' && !r.death));
+ assert.ok(rows.every((r) => /plant material|resin/.test(r.label)));
+});
+
+test('Singapore separates actual possession from three rebuttable trafficking triggers (#279)', () => {
+ const c = country('sg');
+ assert.match(c.segments.possession.text, /above 15 g.*10 g of resin or 30 g of mixture/);
+ assert.equal(c.segments.possession.penalties.length, 9);
+ assert.ok(c.segments.possession.penalties.every((r) => r.term === 'Possession' && !r.death));
+ assert.ok(c.segments.possession.penalties.filter((r) => /resin/.test(r.label)).every((r) => /Above 10 g.*unless rebutted/.test(r.note)));
+ assert.ok(c.segments.possession.penalties.filter((r) => /mixture/.test(r.label)).every((r) => /Above 30 g.*unless rebutted/.test(r.note)));
+ assert.ok(c.segments.sharing.penalties.some((r) => /resin: more than 200 g/.test(r.label) && r.death));
+});
+
+test('Iraq weighing discrepancies remain criminal offences (#281)', () => {
+ const r = country('iq').segments.possession.penalties.find((r) => /33\(2\)/.test(r.label));
+ assert.equal(r.treatment, 'criminal');
+ assert.equal(r.fine_min, 2000000);
+ assert.equal(r.fine, 5000000);
+ assert.match(r.note, /repeat offences carry imprisonment/);
+});
+
+test('Libya and Sudan supply minimums cannot become maximums (#282, #283)', async () => {
+ const { punishmentText } = await import('../src/lib/penalties.js');
+ const ly = country('ly').segments.sharing;
+ assert.match(ly.text, /at least ten years/);
+ assert.match(ly.text, /1996 replaced the penalties/);
+ assert.ok(ly.penalties.every((r) => !/Supply with intent|Supply without payment/.test(r.label)));
+ assert.ok(ly.sources.includes('law1996'));
+ const sd = country('sd').segments.sharing.penalties[0];
+ assert.equal(sd.prison, undefined);
+ assert.equal(punishmentText(penaltyRow(sd)), 'At least 10 years');
+});
+
+test('hemp map categories describe growing thresholds, not product residues (#285–#289)', () => {
+ for (const id of ['om', 'ph', 'tw']) assert.equal(country(id).context.hemp.status, 'none', id);
+ assert.equal(country('gh').context.hemp.status, 'standard');
+ assert.equal(country('gh').context.production.status, 'hemp');
+ assert.equal(country('co').context.hemp.status, 'high');
+ assert.match(country('co').context.hemp.text, /less than 1%/);
+ assert.match(country('tw').context.hemp.text, /finished product.*10 ppm/);
+});
+
+test('Guyana hemp law and fatal-minor capital offence remain distinct (#290, #291)', () => {
+ const c = country('gy');
+ assert.equal(c.context.production.status, 'hemp');
+ assert.equal(c.context.hemp.status, 'standard');
+ assert.match(c.context.hemp.text, /no more than 0.3% delta-9 THC/);
+ assert.equal(c.recreational, 'death');
+ assert.match(c.context.death_penalty.text, /under eighteen.*causes their death/);
+ assert.match(c.context.death_penalty.text, /Ordinary trafficking.*up to life/);
+ assert.ok(c.segments.sharing.penalties.filter((r) => r.death).every((r) => /under eighteen causing their death/.test(r.label)));
+ assert.deepEqual(c.scores, { possession: 2, cultivation: 1, enforcement: 3, sharing: 1, consumption: 2, access: 0 });
+});
+
+test('Suriname uses enacted 2023 hemp law and its actual commencement (#292)', () => {
+ const c = country('sr');
+ assert.equal(c.context.hemp.status, 'high');
+ assert.equal(c.context.production.status, 'hemp');
+ assert.match(c.context.hemp.text, /no more than 1% THC/);
+ assert.doesNotMatch(c.summary, /legalised in 2022/);
+ assert.equal(events().find((e) => e.id === 'suriname-industrial-hemp-2023').date, '2023-09-15');
+});
src/content.config.ts +2 −1
@@ -40,8 +40,9 @@ const penalty = z.object({
caning: z.tuple([z.number().int().positive(), z.number().int().positive()]).optional(), // [fewest, most] strokes
death: z.literal(true).optional(),
+ life: z.enum(["mandatory", "possible"]).optional(), // standalone life or an alternative sentence
fine: z.number().positive().optional(),
fine_min: z.number().positive().optional(), // a minimum fine; alone it reads "at least"
note: z.string().min(1).optional(),
-}).strict().refine((r) => r.upto || r.over || r.label, { message: "a penalty step needs upto, over or a label" });
+}).strict().refine((r) => r.life !== "mandatory" || (!r.prison && !r.prison_min && !r.death), { message: "mandatory life cannot have an alternative prison or death sentence" }).refine((r) => r.upto || r.over || r.label, { message: "a penalty step needs upto, over or a label" });
// a segment: its cited text, and for possession and cultivation optionally the penalty steps
const segment = cited.extend({ currency: z.string().regex(/^[A-Z]{3}$/).optional(), penalties: z.array(penalty).min(1).optional() })
src/lib/penalties.js +15 −2
@@ -58,4 +58,5 @@ export function penaltyRow(row) {
caning: row.caning ?? null, // [fewest, most] strokes of the cane
death: row.death === true,
+ life: row.life ?? null,
fine: row.fine ?? null,
fineMin: row.fine_min ?? null,
@@ -67,7 +68,16 @@ export function penaltyRow(row) {
// "Death penalty"; strokes of the cane go on their own line (punishmentExtra)
export function punishmentText(r) {
- if (r.death) return "Death penalty";
+ if (r.death) {
+ const alternatives = [r.life && "life imprisonment", r.prison && punishmentText({ ...r, death: false, life: null })].filter(Boolean);
+ return alternatives.length === 2 ? `Death, ${alternatives[0]} or ${alternatives[1]}` : alternatives.length ? `Death or ${alternatives[0]}` : "Death penalty";
+ }
+ if (r.life === "mandatory") return "Life imprisonment";
+ if (r.life === "possible") {
+ if (r.prison) return `${punishmentText({ ...r, life: null })} or life imprisonment`;
+ return r.prisonMin ? `${r.prisonMin.text} to life imprisonment` : "Life imprisonment possible";
+ }
if (!r.prison) return r.prisonMin ? `At least ${r.prisonMin.text}` : null; // a minimum with no maximum set
if (!r.prisonMin) return `Up to ${r.prison.text}`;
+ if (r.prisonMin.days === r.prison.days) return r.prison.text;
const [a, ua] = r.prisonMin.text.split(" "), [b, ub] = r.prison.text.split(" ");
return ua.replace(/s$/, "") === ub.replace(/s$/, "") ? `${a}–${b} ${ub}` : `${r.prisonMin.text} – ${r.prison.text}`;
@@ -87,8 +97,11 @@ export function penaltySummary(rows) {
// a row about an amount that includes the small one; a labelled row ("growing as a business") is not
const covers = (r) => (r.upto || r.over) && (size(r.over) ?? 0) < small && (size(r.upto) ?? Infinity) >= small;
+ const smallRows = first.filter(covers), smallLife = smallRows.some((r) => r.life);
return {
freeUpTo: free.length ? Math.max(...free) : 0,
crimeFrom: crime ? size(crime.over) ?? 0 : null,
- smallPrisonDays: first.filter(covers).reduce((m, r) => Math.max(m, r.prison?.days ?? r.prisonMin?.days ?? 0), 0),
+ smallPrisonDays: smallLife ? null : smallRows.reduce((m, r) => Math.max(m, r.prison?.days ?? r.prisonMin?.days ?? 0), 0),
+ smallPrisonLife: smallLife,
+ life: rows.some((r) => r.life),
// whether any step of the ladder allows caning or the death penalty
caning: rows.some((r) => r.caning),
src/lib/segment-ids.js +3 −3
@@ -58,6 +58,6 @@ export const HEMP_STATUS = {
high: { label: "Up to 1% THC", tone: "legal", help: "Plants with up to 1% THC count as hemp, a higher line than the usual 0.3%" },
standard: { label: "Up to 0.3% THC", tone: "decrim", help: "Plants with up to about 0.3% THC count as hemp, the line used in the EU and the United States" },
- strict: { label: "Below 0.3% THC", tone: "partial", help: "Hemp is allowed only under a lower THC line than 0.3%, or under unusually tight conditions" },
- none: { label: "No hemp", tone: "illegal", help: "The law makes no exception for hemp: every cannabis plant, seed and CBD product is a controlled drug" },
+ strict: { label: "Below 0.3% THC", tone: "partial", help: "Hemp cultivation is allowed under a THC limit below 0.3%" },
+ none: { label: "No hemp", tone: "illegal", help: "No general low-THC cultivation exception; product-specific exemptions and medical or research permits may still apply" },
};
// Cannabis industry: who may produce cannabis legally for sale. Not part of the score.
@@ -67,5 +67,5 @@ export const INDUSTRY_STATUS = {
pilot: { label: "Pilots only", tone: "decrim", help: "Legal supply for adults exists only in trials or experiments; elsewhere only medical production is licensed" },
medical: { label: "Medical only", tone: "partial", help: "Licensed production exists for medicine, research or export, but not for adults' recreational use" },
- hemp: { label: "Hemp only", tone: "partial", help: "Only low-THC hemp may be grown or processed under licence; no cannabis is produced legally for medicine or adults" },
+ hemp: { label: "Hemp only", tone: "partial", help: "Licensed production is limited to low-THC hemp; this category does not establish a higher-THC cannabis industry" },
none: { label: "None", tone: "illegal", help: "No legal cannabis production of any kind" },
};
src/lib/seo.js +1 −1
@@ -38,5 +38,5 @@ const REC_SENTENCE = {
decrim: "is decriminalised but still prohibited",
illegal: "is illegal",
- death: "is illegal, with the death penalty for trafficking",
+ death: "is illegal, with the death penalty for specified cannabis offences",
};
const MED_SENTENCE = { yes: "Medical cannabis: legal.", limited: "Medical cannabis: limited.", no: "No patient cannabis treatment route verified." };
src/pages/compare.astro +4 −3
@@ -95,4 +95,5 @@ const presets = [
<script>
+ import { punishmentText } from "../lib/penalties.js";
import { tierOf, tierColor } from "../lib/tiers.js";
@@ -159,5 +160,5 @@ const presets = [
// a segment's penalty steps (src/lib/penalties.js), as a compact table under its text
const money = (n, cur) => { try { return new Intl.NumberFormat("en-GB", { style: "currency", currency: cur, maximumFractionDigits: 0 }).format(n); } catch { return `${n} ${cur}`; } };
- const punish = (r) => r.death ? "Death penalty" : r.prison && r.prisonMin ? `${r.prisonMin.text} – ${r.prison.text}` : r.prison ? `up to ${r.prison.text}` : r.prisonMin ? `at least ${r.prisonMin.text}` : "";
+ const punish = (r) => punishmentText(r) ?? "";
const fineOf = (r, cur) => r.fine && r.fineMin ? `${money(r.fineMin, cur)}–${money(r.fine, cur)}` : r.fine ? `up to ${money(r.fine, cur)}` : r.fineMin ? `at least ${money(r.fineMin, cur)}` : "";
const penaltyTable = (pen) => !pen?.rows?.length ? "" : `<ul class="cmp-pen">${pen.rows.map((r) => {
@@ -300,5 +301,5 @@ const presets = [
const v = vals[k];
if (v == null) return pen(c, seg) ? `<span class="cmp-sub">—</span>` : none(c);
- const text = unit === "days" ? (v === 0 ? "No prison" : daysText(v)) : v === 0 ? zero : amountText(v, unit);
+ const text = v === "life" ? "Life imprisonment" : unit === "days" ? (v === 0 ? "No prison" : daysText(v)) : v === 0 ? zero : amountText(v, unit);
return `<div class="cmp-val${v === best ? " best" : ""}"><b>${text}</b></div>`;
})));
@@ -306,5 +307,5 @@ const presets = [
figure("Possession without prosecution<small>legal or tolerated, up to</small>", "possession", (s) => s?.freeUpTo, "g", 1, "Nothing");
figure("Possession becomes a crime<small>from</small>", "possession", (s) => (s ? s.crimeFrom : null), "g", 1, "Any amount");
- figure("Prison for a small amount<small>the law's maximum for 5 g</small>", "possession", (s) => s?.smallPrisonDays, "days", -1, "No prison");
+ figure("Prison for a small amount<small>the law's maximum for 5 g</small>", "possession", (s) => s?.smallPrisonLife ? "life" : s?.smallPrisonDays, "days", -1, "No prison");
figure("Plants without prosecution<small>legal or tolerated, up to</small>", "cultivation", (s) => s?.freeUpTo, "plants", 1, "None");
figure("Growing becomes a crime<small>from</small>", "cultivation", (s) => (s ? s.crimeFrom : null), "plants", 1, "Any plant");