--- name: legal-acts-data description: Search and retrieve official Estonian legislation through the Riigi Teataja legal-acts JSON API and linked act representations. module: riigiteataja.mjs execution: get --- # Riigi Teataja Legal Acts ## Vetted module (use this first) `riigiteataja.mjs` ships in https://kodanikukratt.ee/kratt-agent.tar.gz (plain ESM, Node 22+) and is re-verified against the live source: ```js import { show, save } from "./kratt.mjs"; import { openAct, findInAct } from "./riigiteataja.mjs"; const act = await openAct("Töölepingu seadus"); // the redaction in force TODAY show({ id: act.globaalID, title: act.title, status: act.status, from: act.inForceFrom, to: act.inForceTo, url: act.url, note: act.note }); save(`akt_${act.globaalID}.html`, act.html); // ~550 KB — load() it later, never print it whole // One script, every § the answer will name: the § the question is about AND // the neighbouring rules the reader acts on. The three terms below are this // question's (notice periods, compensation, deadline to contest) — pick your // own, but pick them all up front. Naming a § you never printed is how wrong // law gets stated. for (const term of ["etteteatamise", "§ 100", "§ 105"]) { for (const hit of findInAct(act.html, term, { max: 2 })) { show(hit.text, 4000); // hit.text IS one whole § — never truncate it } } ``` - `openAct(title)` does the whole in-force dance: PAGED title search → the act that title names → validity window covers today → newest first → confirm `aktiStaatus: "KEHTIV"` → fetch the text. A title search returns one row per historical redaction, so never just take the first hit. - **The title does not have to be exact.** An inflected or reordered one resolves ("koerte ja kasside pidamise eeskirjas Saaremaa vallas" → the act titled "Koerte ja kasside pidamise eeskiri Saaremaa vallas"), so pass the title as the page you read writes it rather than guessing at a base form. When the resolution was not exact the result carries a `note` with the register's own title — **cite that one**. - **It THROWS rather than choosing between acts, and the message says what to send back.** Two shapes, both meaning "name it and call again": - several titles matched — call `openAct` again with one of the titles it lists, copied verbatim; - one title, several issuing bodies — 64 municipalities have an act titled exactly "Koerte ja kasside pidamise eeskiri", so pass `openAct(title, {issuer: "Saaremaa Vallavolikogu"})`, or ask again with a title that names the place. Which body issued a local regulation IS the answer to a question about one place; reading whichever came first gives the reader another municipality's rules in a form they cannot tell from their own. - **It also THROWS when it cannot confirm which redaction is in force.** That is the correct outcome, not a tool failure to work around: say the current text could not be confirmed and link the act. Do NOT fall back to a redaction you found some other way — a superseded act reads exactly like a current one, and the reader cannot tell. - Two traps this module handles, and any hand-rolled fallback must handle too (issue #85). **The search is paged**: "Vabariigi Valitsuse seadus" has 227 rows and the redaction in force is on page 3, so a single `limiit=200` request does not contain it at all. And **`kehtivus.lopp: null` does not mean "still in force"** — 23 of its 90 exact-title rows carry a null end date with start dates back to 2003, every one superseded. Only `aktiStaatus` from the public API decides. - `findInAct(html, term, {max, window})` returns each hit as the **whole `§`** it falls in — heading, every lõige, every punkt — so an enumeration can never arrive cut off. `term` is a phrase ("ülempiir") or a section number ("§ 97"), and the § that DEFINES the term sorts ahead of §§ that merely cite it. Search something specific: "maksimaalne" matches an unrelated elatisabi clause. - `hit.text` is already one complete §, so print it whole — `show(hit.text, 4000)`. A `show(hit.text, 700)` puts the cut back where it was. - `window` raises the per-§ character cap (default 3000) and cannot lower it, so there is no small number to pass by mistake. Omit it. - Do NOT go back to `html.indexOf("§ 97")` + `substring()`. Act HTML is ~85% hover-menu markup, so a 2500-character slice holds three sentences of law and stops mid-list; that is exactly what produced wrong law before. - If a § is long enough that some parts were held back, the text says so in `[…NOT printed…]`. Read the rest before relying on them. - Also exported: `findCurrentAct(title, {issuer})`, `actText(globaalID)`, `resolveActTitle(title)`, `searchActs(title)`, `actSections(html)`. - Why the module exists beyond convenience: the `blob-html` route answers **HTTP 406** to the helper's default JSON `Accept` header, so a plain `get()` on it fails. The module sends `Accept: text/html`. Re-verified 2026-08-02 on act 236514: `Accept: application/json` → 406 with an empty body, while `Accept: text/html` and a request carrying **no** `Accept` header both → 200 with 380 KB of act HTML. So a hand-rolled fetch that sets no header will not reproduce the failure, and this claim is only ever true of a caller that asks for JSON. ```probe-limit claim the `blob-html` route answers **HTTP 406** to the helper's default JSON `Accept` header # The act id is fetched rather than written down, and it is checked to resolve # BEFORE the refusal is asserted: this route negotiates the media type before it # looks the act up, so a withdrawn id answers 406 to JSON and 404 to HTML # (measured 2026-08-02) and would satisfy every line below while proving nothing # about the header. GET https://www.riigiteataja.ee/api/oigusakt_otsing/1/otsi?leht=1&limiit=1&pealkiri=riigieelarve expect-status 200 capture akt /"globaalID"\s*:\s*"?(\d+)/ GET https://www.riigiteataja.ee/public-api/api/v1/akt/{{akt}}/blob-html header Accept: text/html expect-status 200 GET https://www.riigiteataja.ee/public-api/api/v1/akt/{{akt}}/blob-html header Accept: application/json expect-status 406 # The control is that same served act asking for HTML: the only difference from # the step above is the one header the claim is about. control GET https://www.riigiteataja.ee/public-api/api/v1/akt/{{akt}}/blob-html header Accept: text/html ``` **Grounding rule:** state a rule, number, deadline or exception ONLY if it is in text you printed in this conversation. Three failures this is written from, all observed: - **Do not finish a list.** Printed points 1)–3) of § 97 lg 2? Point 4) is not yours to state. Print the § again if the output was cut. - **Do not reach into a § you did not fetch.** Naming the citizen's options is required of you, and it is exactly where memory creeps in: an answer on notice periods asserted § 100 and § 105 without fetching either, and put the redundancy payment at two months' average wage where § 100 lg 1 says **one**. This covers anything you STATE, not only what you cite by § number — "the general deadline for contesting it is 30 days" is the same claim with the citation removed. - **Fetch them.** Every § the answer will touch goes in the SAME script as the § the question is about: `for (const t of ["§ 97", "§ 100", "§ 105"])`. It costs no extra step. - **Did not fetch it? Then name the topic and link the act, without a number.** "Töölepingu seadus reguleerib ka koondamishüvitist ja ülesütlemise vaidlustamise tähtaega — täpsed tingimused on aktis." That satisfies the obligation to say what the reader can do, and invents nothing. It is always the better answer than a plausible figure. - **Do not restate a value from memory once you have the right §.** § 40 of Perehüvitiste seadus was located and answered "kolmekordne"; the fetched text says "kahekordne". If a § could not be retrieved, say so and link the act. An honest gap is worth more than a fluent paragraph the reader cannot check. The endpoints below are the fallback if a module call throws. ## Access Public JSON search API and linked XML act texts. No authentication. ## Retrieve Start with a bounded search: ```text GET https://www.riigiteataja.ee/api/oigusakt_otsing/1/otsi?leht=1&limiit=25&pealkiri=riigieelarve ``` Useful query parameters include `leht` (page), `limiit` (page size), and `pealkiri` (title text). Add filters from the Riigi Teataja search UI only after confirming their exact URL names. The response contains: - `staatus` and `paring`; - `metaandmed.kokku`, `metaandmed.leht`, and `metaandmed.limiit`; - `aktid`, including `globaalID`, `terviktekstID`, `pealkiri`, `liik`, `valjaandja`, `kehtivus`, `staatus`, and relative `url`. Resolve a returned `url`, such as `/akt/22451.xml`, against `https://www.riigiteataja.ee` to retrieve the official act representation. ## Return Preserve legal IDs, title, type, issuer, validity start/end, publication status, text type, act URL, search parameters, page metadata, and retrieval time. Clearly distinguish current and historical versions. ## Limits - A broad unfiltered search is valid but returns historical as well as current acts. - Legal validity must be read from the returned version metadata, not inferred from search order. - Do not rewrite relative act URLs incorrectly; resolve them against the Riigi Teataja origin. - When citing an act to the user, link a URL built only from fetched identifiers: `https://www.riigiteataja.ee/akt/{globaalID}` or the search row's relative `url` (drop the `.xml`) resolved against the origin. Never type an abbreviation-style URL (`/akt/VÕS`) from memory: the abbreviation lives in the fetched `lyhend` field and must be copied verbatim if used at all — ASCII-folding it (VÕS → VOS) produces a dead link that still returns HTTP 200, because the SPA shell answers every path and fails only client-side. ## Verify Require HTTP 200 JSON, `staatus: OK`, integer pagination metadata, and a parseable `aktid` array. At least one returned act must contain `globaalID`, `pealkiri`, `kehtivus`, and `url` before reporting success. ## Contract drift warning (verified 2026-07-24) Resolving act representations like `/akt/{id}.xml` against `https://www.riigiteataja.ee` now returns the HTML application shell (SPA), NOT the act text — regardless of Accept header. Do not treat that HTML as act content, and do NOT recite paragraph (§) text from memory as if it were fetched. Working text endpoints (verified 2026-07-24): - `GET https://www.riigiteataja.ee/public-api/api/v1/akt/{globaalID}` — act metadata JSON. - `GET https://www.riigiteataja.ee/public-api/api/v1/akt/{globaalID}/blob-html` — full consolidated act HTML. Responses can exceed 500 KB and the endpoint ignores Range headers, so a truncating fetch returns only the head of the act. Filter it where you fetched it rather than reading it whole; if you cannot, take the head as all you have — cite the paragraphs it actually contains, and for anything past the cut point say so and link the act instead of reciting it from memory. If those endpoints are unavailable, ground answers in the search API's metadata only (title, validity, status, act URL), summarize at that level, and link the act for the user to read the text. ## EU origin: EUR-Lex For a law transposing EU law, the seletuskiri (via `riigikogu-open-data` draft texts) cites a CELEX number, e.g. 32019L1937. A plain server-side fetch of `eur-lex.europa.eu` gets an empty HTTP 202 (WAF challenge) — fetch `https://publications.europa.eu/resource/celex/{CELEX}` with headers `Accept: application/xhtml+xml` and `Accept-Language: et`, both mandatory (verified 2026-07-27; ~450 KB XHTML of the full Estonian text — filter it down before reading, never take it whole). **Mandatory means the request fails silently without them**: the same URL sent with no `Accept` answers HTTP 200 with 11.7 MB of `application/rdf+xml` describing the act — its dates, languages and identifiers, not a word of its text (verified 2026-08-03 with node). Cite for the reader: `https://eur-lex.europa.eu/legal-content/ET/TXT/?uri=CELEX:{CELEX}`. ## Module API openAct(title, {issuer}) -> {globaalID, title, issuer, inForceFrom, inForceTo, status, url, html, note} findInAct(html, term, {max, window}) -> [{paragraph, heading, text}]