--- name: legislation-workflow-eis description: Draft laws in EIS - current coordination and consultation rounds, kooskõlastuskirjad and märkuste tabel. module: eis.mjs execution: code --- # Draft Legislation Pipeline (EIS) The record of a draft law **before it reaches the Riigikogu**: what is in ministry coordination or public consultation right now, and — for any draft, current or years past — the coordination round's actual files: kooskõlastuskirjad, the märkuste/kooskõlastustabel (each objection and the ministry's answer), and public arvamused. Once a bill is in parliament, `riigikogu-open-data` covers the parliamentary stage; the ministry stage stays here. ## Access - Current listings: `https://eelnoud.valitsus.ee/main/mount/share/home` (also served at `/main`) - One draft's toimik (its whole file): `https://eelnoud.valitsus.ee/main/mount/docList/` — a stable, direct-GET URL - Public server-rendered HTML, no login. Send a normal browser `User-Agent`. - Legacy OSALE URLs redirect here. ## Retrieve: current listings `eisDrafts({stage})` parses the three tables by their headings: | heading | stage | columns | |---|---|---| | `Avalikuks konsulteerimiseks esitatud eelnõud` | public consultation | Pealkiri, EIS number, Algataja viide, Algatatud, **Tähtaeg**, Staatus, Liik | | `Kooskõlastamiseks esitatud eelnõud` | inter-ministry coordination | same, with Tähtaeg | | `Vabariigi Valitsusele esitatud eelnõud` | submitted to government | same, **no** Tähtaeg | The stage is the heading, not a column, and each row carries its own. `open: true` is computed from the row's Tähtaeg not having passed AND its Staatus still being `Teostamisel` — that is a live deadline a citizen can act on, and it is the thing worth surfacing. `Lõppenud` means the window has closed. EIS-number prefixes name the ministry and the module resolves them: `JDM` Justiits- ja Digiministeerium, `REM` Regionaal- ja Põllumajandusministeerium, `SIM` Siseministeerium, `MKM` Majandus- ja Kommunikatsiooniministeerium, `RAM` Rahandusministeerium, `VÄM` Välisministeerium, `SOM` Sotsiaalministeerium, `KLIM` Kliimaministeerium, `KAM` Kaitseministeerium, `HTM` Haridus- ja Teadusministeerium, `KUM` Kultuuriministeerium. ## Retrieve: one draft's toimik and its files The listing's row links are JavaScript, so the toimik UUID comes from outside EIS: 1. **Riigi Teataja draft-acts API** (any draft, verified back to 2017): the EIS number IS the RT `menetluseId`. `POST https://www.riigiteataja.ee/public-api/api/v1/avalik/eelnou` with `{"menetluseId":"SOM/24-0227"}` → each stage's `menetlusTeave` is a `docList/` URL. No EIS number? Search by title first — see `riigiteataja-draft-acts`. 2. **Riigikogu volume** (bills that reached parliament): the draft detail's `texts[]` holds a file titled `EIS teade`, named `[EIS]_…_SOM-24-0227_….txt` — the filename carries the EIS number (`-` for `/`), and the .txt itself contains the docList link. For a draft neither source resolves, the fallback is the drafting ministry's own document register (`ministry-document-registries`): search the agency named by the EIS-number prefix by `title=` keyword. `eisDossier(uuid)` fetches that page — ~380 KB of HTML, never through a raw tool response — and returns: - the **Kooskõlastused** rounds: each coordinating ministry with its `Tähtaeg`, `Vastatud` and verdict (`Kooskõlastatud märkustega`, `Kooskõlastatud` …); - every **document** with its registration number, date and file list. The ministry response documents are the kooskõlastuskirjad; the VV-submission document's files include the seletuskiri annex `…KOOSKÕLASTUSTABEL….pdf` (the märkuste tabel); - **Avalikud arvamused** — the public comments from interest groups and law firms, which hang off the activity rather than a registered document and so are absent from the page's own flat "all documents" block. `eisFileText(uuid, name)` then returns one file as text, picking it by name from that list. ### Why the download is a module function and not a script here Getting a file out of EIS is three requests sharing one session: GET the toimik and keep its `JSESSIONID`; POST the file's widget path and index to be handed a fresh URL; GET that URL with the same cookie. Two strings have to be copied **verbatim** out of an escaped `onclick` handler, and the third URL is **single-use** — a second GET answers HTTP 200 with 380 KB of toimik HTML and no `content-disposition`, which to anything checking only the status code is indistinguishable from the file. The module does all of it in one call, checks `content-disposition` before treating bytes as a document, skips the server's `N;PDF` re-renderings, and hands the result to `fileText()`, so an `.asice` comes back with the documents inside already read. Worked example (verified 2026-08-02): `SOM/24-0227` (Magustatud joogi maksu seadus, later 418 SE) → toimik `50dc6369-567c-4827-aff7-7726e960ef75`; 4 coordination rounds — MKM, Justiitsministeerium, Regionaal- ja Põllumajandusministeerium, Rahandusministeerium, all `Kooskõlastatud märkustega`, 28.03–01.04.2024 — 9 documents and 45 files. `eisFileText(uuid, "KOOSKÕLASTUSTABEL")` returns 131,294 characters, each objection next to the ministry's answer. The 2017 toimik `RAM/17-0515` resolves the same way — depth reaches at least 2017. ## Return - Listings: preserve title, EIS number, algataja viide, algatatud date, tähtaeg, staatus, liik, the listing heading, source URL and retrieval time. Say which stage a row is in. - Toimik: preserve the EIS number, the docList URL (cite it — it is a page a citizen can open), each coordination row's ministry/dates/verdict as printed, and the exact filenames of files you quote from. - Never present an EIS record as enacted law — for text in force use `legal-acts-data`. ## Limits - **The listing pages cannot reach the toimikud.** Row links are `ev()` JavaScript and `Täpne otsing` is an Aranea stateful form that cannot be driven over HTTP — toimik UUIDs come only via Riigi Teataja or a Riigikogu volume, so a draft too fresh or too obscure for either has no route. The listing itself retains completed rows for a while but is not an archive and cannot be paged back. - **The three RSS feeds are refused** — `publicConsult.rss`, `review.rss` and `submission.rss` under `/main/mount/rss/home/`, each HTTP 403 `You don't have permission to access this resource.` (all three re-checked 2026-08-02). Cloudflare fronts the host but this is the origin refusing, not a challenge to solve: do not use the feeds and do not try to work around it. ```probe-limit claim The three RSS feeds are refused GET https://eelnoud.valitsus.ee/main/mount/rss/home/publicConsult.rss expect-status 403 expect /You don't have permission to access this resource/ # The control is EIS's own front page. If the site as a whole went behind a # challenge every path would answer 403, this claim would keep "holding", and # the much larger finding — EIS is unreachable — would be the one nothing said. control GET https://eelnoud.valitsus.ee/main ``` - **We cannot get a file out of a toimik with plain requests.** The download is the three requests above sharing one session, with two strings copied verbatim out of the page's own JavaScript and a handover URL that works once, and the kooskõlastustabel is usually a container that has to be unpacked — so without a runtime, read and cite the docList page and link the file rather than quoting from it. - Files with access restrictions (AK) can be withheld even when the row is public; anonymous sessions see only public material. - **Sätla does not replace this yet, and will not backfill it.** The riigi koosloome keskkond created by 879 SE (passed 2026-07-03) is a phased, opt-in rollout: drafts begun in Sätla are *exported* to continue in EIS, first-stage public consultation still happens in EIS, and permanent retention under RTS § 12¹ lg 8 covers only acts processed in Sätla. EIS stays live and remains the sole home of historical coordination material. Re-check when Sätla's later stages land. ## Verify - Listings: require HTTP 200 `text/html` containing all three listing headings and at least one row whose EIS number matches `[A-ZÕÄÖÜ]{2,4}/\d{2}-\d{4}`. When claiming a deadline is open, require the row's own `Tähtaeg` and `Staatus` in the fetched HTML. - Toimik: require the page `` to name the draft you asked about; a login prompt or an empty shell is not the toimik. - Downloads: require `content-disposition` (or the file's magic bytes) on the step-3 response before quoting from it — an HTTP 200 without it is the toimik HTML, not the document. State file contents only from text you actually extracted and printed. - Reject a Cloudflare challenge page anywhere in the flow — it is a block, not data. The worked example above is also the live acceptance check, run weekly by `scripts/check-sources.mjs` (issue #82). It walks the whole three-request download, because that is where the silent failure lives: ```probe GET https://eelnoud.valitsus.ee/main/mount/docList/50dc6369-567c-4827-aff7-7726e960ef75 expect /<title>[^<]*Magustatud joogi maksu seadus/ # Only a plain numeric <PARAM>: a `N;PDF` index would need URL-encoding and # tests the server's PDF renderer rather than the download flow. capture widget,param /downloadFile\(\\'download\\',\\'([^'\\]+\.documentList\.[^'\\]+)\\',\s*\\'(\d+)\\'/ POST https://eelnoud.valitsus.ee/main?araTransactionId=override&araServiceActionPath={{widget}}&araServiceActionHandler=download&araServiceActionParameter={{param}} expect /^\s*https:\/\/eelnoud\.valitsus\.ee\S+\s*$/ capture once /(https:\/\/\S+)/ GET {{once}} expect-header content-disposition # The step-3 URL is single-use: a second GET answers 200 with 380 KB of toimik # HTML and no content-disposition. To anything that only checks the status code # that is indistinguishable from the file, which is why it is asserted here. counter GET {{once}} ``` ## Module API eisDrafts({stage}) -> {source, page, totals, rows, note} eisDossier(uuid) -> {source, page, uuid, title, totals, rounds, documents, note} eisFileText(uuid, name) -> {source, page, name, format, chars, text, note}