# This file contains metadata for your plugin.

# This file should be included when you package your plugin.
# Mandatory items:

[general]
name=Cartogen AI
qgisMinimumVersion=4.2
qgisMaximumVersion=4.99
description=Spatial AI that plans, executes, and shows its work inside QGIS.
version=1.16.1
author=Alaa Alshoubaki
email=cartogenai@gmail.com

about=Cartogen AI is a spatial agent for QGIS: describe what you want in plain language, and it plans the steps, calls the right tools, and executes real operations against your open project -- every step inspectable, not just suggested. Powered by OpenRouter, Google Gemini, or a local Ollama model. No expensive API key strictly required -- get a free OpenRouter key at openrouter.ai, or run Ollama locally for free. REQUIREMENTS: QGIS 4.2 or later (Qt 6); an internet connection and an API key for one cloud model provider (OpenRouter or Gemini), or a local Ollama server. With a cloud provider, your prompts and the project data the agent reads to answer them are sent to that provider (see SECURITY.md in the repository for what is protected and how). PYTHON PACKAGES: requests, numpy and GDAL come with QGIS. Optional packages for document attachments, web search, charts and web dashboards (pypdf, python-docx, openpyxl, pandas, pdfplumber, matplotlib, folium, ddgs) are installed through the qpip plugin (a declared plugin dependency) or with pip install -r requirements.txt. The optional imagery feature extraction also needs ultralytics (AGPL-3.0 licence) and downloads a model file the first time it is used.

tracker=https://github.com/http-solution/cartogen-ai-qgis-plugin/issues
repository=https://github.com/http-solution/cartogen-ai-qgis-plugin
license=GNU GPL v2

# End of mandatory metadata

# Recommended items:

hasProcessingProvider=yes
# Declares Qt 6 support for QGIS 4 plugin managers that read this key; managers that do not know it ignore it (added by the rc28 self-audit; confirm against the directory page after the next upload).
supportsQt6=True
changelog=v1.16.1 (2026-10-14):
    No breaking changes -- requires QGIS 4.2+. The stable build of the 1.16 line; same code as rc41.
    - First stable release of 1.16: 1.16.1 is the stable build of the 1.16 line. It contains exactly the code of 1.16.0-rc41 and nothing else: the version number no longer carries 'pre-release'. The 1.16 line adds the audit fixes, the cost and routing work, the incident, earthquake, conflict-event and fact-check tools, point-position flags, and the fixes from the rc37 and rc40 hand tests (rc20 to rc41; full list in CHANGELOG.md).
    - What was hand-tested: The rc40 build was hand-tested on Windows with QGIS 4.2.2 and Gemini (67 rows: 42 pass, 14 fail, 11 not run); 19 issues were closed from it and the failures were fixed in rc41 or traced to wrong test rows. Upgrading from rc39 to rc40 was checked by the owner. The rc41 fixes are checked by the offline suite and CI only, including live checks in QGIS 4.2.2.
    - Not yet hand-tested in 1.16.1: Stop during a model call, the raster-style follow-up, honest script edits to file layers, task-template defaults, the routing words for access by road, the Memory dialog line and the cloud-card text. The corrected live-test rows for them are in docs/FULL_LIVE_TEST_open_issues_ROWS.csv. Report anything that fails at https://github.com/http-solution/cartogen-ai-qgis-plugin/issues.
    - Changes from rc41: None in code. Version number and release text only.

v1.16.0-rc41 (2026-10-14):
    No breaking changes -- still requires QGIS 4.2+. Fixes from the rc40 hand test: Stop during a model call, honest script edits, raster styling kept, template defaults only where used.
    - Stop takes effect during a model call: Pressing Stop while the assistant was waiting for the model took 25 to 30 seconds to show, because the wait only ended when the reply did. The wait now checks Stop every quarter of a second, ends the turn as 'Stopped by user' rather than an API error, and does not resend the request. Checked by offline tests only; not tried against a real provider.
    - Script edits to file layers are reported honestly: A script works on a copy of a file or database layer, so an edit it made to such a layer never reached the real one, yet the reply said it had. The reply now says the edit was NOT applied and points to an editing tool. Layers a script creates are flagged as temporary (lost when the project closes unless saved or exported). Memory layers still receive same-count edits. Two new live checks run in QGIS 4.2.2 in CI.
    - A styled raster stays styled: After the assistant applied a colour ramp to a raster, a follow-up that checks the promised result told it to call a categorized-style tool, and it re-stretched the raster to grey. Styling tools now count as delivering a layer, so no follow-up is sent.
    - Task templates state a default only where it is used: A matched task template used to add 'Given: threshold = 5 km', 'imagery = most recent low-cloud scene' or 'population_src = WorldPop' to requests that never mentioned them. A default is now stated only when a tool in the task's chain can use that slot (issue 304, owner decision).
    - Routing words for access by road: 'Which facilities are beyond N minutes or km by road' now points the assistant to classify_facilities_by_access, the tool that adds the green reached and red beyond layer, for any number of facilities (the prompt said more than about 200).
    - Incident wording, Memory dialog, cloud card, busy chat: find_incident_reports tells the model to say 'reported by', never 'verified'. The Memory dialog says whether notes are saved with the project. The cloud-override card says that layer names, field names, types, CRS and counts always reach the cloud model. A message sent while a reply is running now says it was kept in the box instead of being ignored silently.
    - Security scan notes: Bandit's two XML findings (B405, B314) are marked with the reason: both feed parsers refuse any DOCTYPE or ENTITY and cap the size before parsing.
    - Not hand-tested: Everything above is checked by the offline suite and CI (including live checks in QGIS 4.2.2) only. Stop latency, the card and dialog text, the raster fix and the routing words need a hand run; the corrected live-test rows are in the bundle.

v1.16.0-rc40 (2026-10-13):
    Release candidate 40 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Adds a fact-check lookup, flags point positions the model typed, and fixes downloads behind a proxy.
    - New tool: search_fact_checks: Answers 'has this claim, video or photo been fact-checked?' from the published feeds of Snopes, PolitiFact, FactCheck.org, Full Fact, Lead Stories and the Arabic-language Fatabyyano (searchable in Arabic). Every reply names the outlets that answered or failed and says that only each outlet's newest 10 to 25 entries are searched, so no match is never evidence that a claim is true, false or unchecked. Reuters, AFP, Africa Check and AP refuse automated requests and cannot be asked. The six feeds were queried live from a development session; not hand-tested in QGIS.
    - Point positions the model typed are flagged: Coordinates passed to add_point_layer, add_incident_point or execute_pyqgis_script that appear in neither your message nor an earlier tool result are flagged in the tool result and in the final reply ('Unverified point positions'). This closes the gap noted in rc39. It never blocks, and it cannot see coordinates computed or read from a file inside a script.
    - Routes from a depot in another layer: optimize_delivery_route takes additional_stops_layers, so a depot or hub in one layer and the stops in another no longer need scripts to merge them first (rc37 row F3 took 9 calls for 5 stops). Whether the assistant now does it in one call is not yet checked.
    - Analysis fields are written through the edit session: Severity score, presence-gap status, population-in-need and damage-severity fields were added straight to the data provider, bypassing the edit buffer, so a failure half-way could not be undone and a layer you were already editing was ignored (issue 293). They now go through the same checked, undoable edit as the other field-writing tools.
    - Downloads behind a proxy: A download that follows a redirect (geoBoundaries boundary files do) used urllib for the redirected hop, which does not know a PAC or authenticated proxy that QGIS does. A hop urllib cannot connect to is now retried through QGIS's own network stack. geoBoundaries failures now say whether the lookup or the file download failed, and from which host. The tester's failure on 2026-10-13 is still unconfirmed; this fixes the best-supported explanation.
    - Security alerts and documentation site: Fixes for the CodeQL alerts on the website prototype (content security policy, a reflected error message, a log format string), a test server that copied the Host header into a redirect, workflow permissions, and the documentation site's search. The documentation site is live at https://http-solution.github.io/cartogen-ai-qgis-plugin/.
    - Not hand-tested: Everything above is checked by the offline suite and CI (including the live checks in QGIS 4.2.2) only. The proxy fix was not tried behind a real proxy, the point-flagging and the depot merge were not tried with a real model, and the rc39 hand-test rows (no invented layer, follow-up, Stop on a long job) are still to run.

v1.16.0-rc39 (2026-10-13):
    Release candidate 39 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. The assistant no longer invents records to fill an empty map layer.
    - No invented records to fill an empty layer: When a request promised a map layer and no layer had been made, the follow-up the plugin sends the model told it to 'generate an operational/representative sample dataset'. In the rc37 hand test (a request to check the internet for incidents near Amman, after every source returned nothing) the model added four invented incidents to the map as a 'planning baseline'. The follow-up now says the opposite: do not invent, sample, simulate or illustrate records; say which sources were tried, what each returned, and that no layer was made because there was nothing real to show. A test fails if the old wording returns.
    - Hand-test result for rc37, corrected: Row RC37-A1 of the live test, recorded as a pass by the tester, is a fail for the reason above. Everything else in the rc37 report stands (docs/RC37_LIVE_TEST_REPORT_2026-10-13.md).
    - Live-test bundle: The live-test bundle (tools/build_live_test_bundle.py) takes its version from metadata.txt, and its rows no longer name a version.
    - Not hand-tested: The fix is checked by the offline suite and CI only. Nothing stops the model from adding made-up points by another route (for example add_point_layer with coordinates it invented); this release only removes the wording that asked for it.

v1.16.0-rc38 (2026-10-13):
    Release candidate 38 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes the Processing-algorithm tool, which failed on every call since rc24, and a false 'step did not run' note after incident searches.
    - Processing-algorithm tool works again: run_allowlisted_processing_algorithm failed on every call from the assistant since rc24 with "unexpected keyword argument 'params'": a code change had left the tool registered against an internal helper. Found in the rc37 hand test (a follow-up statistics step after a classification). The registration is fixed and a new test checks that every registered tool accepts the arguments it advertises.
    - No false 'step did not run' note after incident searches: After find_incident_reports added its layer the chat could still end with 'Expected styled layers ... but the step that writes it did not run', because the tool's name was not recognised as one that creates a layer. It is now.
    - rc37 hand test: rc37 was hand-tested on 2026-10-13 against Gemini in desktop QGIS 4.2: 17 of 23 rows passed, 2 were inconclusive (Stop was never exercised against a running job), 2 were not recorded and 2 optional rows were skipped. Report: docs/RC37_LIVE_TEST_REPORT_2026-10-13.md.
    - Not hand-tested: The two fixes above are checked by the offline suite and CI only; neither has been re-run in desktop QGIS. Stop during a long-running job is still unproven on real data.

v1.16.0-rc37 (2026-10-12):
    Release candidate 37 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Adds find_incident_reports, an incident search over named reliable sources with provenance on every record.
    - New tool: find_incident_reports: Answers 'what incidents, disasters or conflict events happened in <place>' from a fixed list of named sources instead of the open web: GDACS (UN alerts), USGS earthquakes, NASA EONET, UCDP armed-conflict events and the ReliefWeb disasters feed. Every record carries its source, rating, date and link; records with a position become a map layer (ReliefWeb items have no coordinates and are listed). Every reply names the sources that answered, the ones that could not be reached and the ones not searched, so an empty result is not read as 'nothing happened'. UCDP data is preliminary and weeks old. No keys needed. The tool count is now 211.
    - What it does not search: News outlets, fact-checkers and social media are not searched: the Reuters, AFP and Full Fact pages cannot be read by a program, Liveuamap is a paid API, ACLED needs a registered key, and social media is out of scope. The reply lists each with its reason.
    - search_web points to it: The general web search description now tells the assistant to use find_incident_reports for incident, disaster and conflict questions.
    - Not hand-tested: The five sources were queried live from a development session and CI is green, including the live checks in QGIS 4.2.2, but none of this has been hand-tested in desktop QGIS. Whether the assistant chooses the new tool for a plain request such as 'check the internet for incidents' is not yet known.

v1.16.0-rc36 (2026-10-12):
    Release candidate 36 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes several causes of QGIS freezing during long analyses, classifies large rasters in blocks and bounds the chat window.
    - QGIS stays responsive during long analyses: Loops over features, pairs and pixels used to run in one unbroken stretch on QGIS's main thread, so QGIS showed Not Responding and ignored Stop. About 90 such loops in the analysis, validation, styling, export and import tools now work in slices of about 50 ms: the window repaints, Stop is heard, and progress is reported every few seconds. A Stop inside an edit rolls the edit back.
    - Hub siting, allocation and delivery routes: The distance matrix is built in slices and the ranking, facility selection and route ordering run off the main thread. Route ordering now finds tours about 14 percent shorter on a 200-stop test and runs faster (it used to stop after 2000 trial moves). The route-risk tool merges geometry in one pass and tests incidents against a prepared buffer.
    - Large raster classification: unsupervised_classification reads and classifies the raster in blocks, so memory no longer limits its size; a raster over 400 million cells is refused with a clip-it-first message (it used to refuse over 25 million cells).
    - Fewer freezes elsewhere: geocode_batch (up to 25 lookups with pauses) no longer runs on the main thread and stops when you press Stop. Scheduled recurring workflows now skip a tick while another one or a chat request is running instead of starting a second analysis on top. The chat window removes the oldest messages once it holds more than 300 items, keeping the newest 200 (the saved conversation is untouched).
    - Not hand-tested: CI is green, including the live checks in QGIS 4.2.2, but none of this has been tried on a large real project or hand-tested in desktop QGIS. The size of the speed-up on real data has not been measured, and 300 and 200 for the chat window are estimates.

v1.16.0-rc35 (2026-10-11):
    Release candidate 35 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Research requests now end with an answer, adds USGS earthquake and UCDP conflict-event tools, and moves URL downloads onto QGIS's network stack.
    - Research requests end with an answer: A request that only reads (a web search, an inspection) never creates or changes anything, so the stall guard used to stop it after six rounds as 'NOT complete' with nothing summarised. Now, when such a turn has gathered results, the model is asked once to answer from them without calling more tools, saying what it found, its sources and what it could not find. A second stall still stops the turn.
    - Two new tools: earthquakes and conflict events: fetch_usgs_earthquakes loads recent USGS earthquakes (bounding box, days, minimum magnitude) as a layer coloured by magnitude band. fetch_ucdp_conflict_events loads UCDP Candidate Events (state-based, non-state and one-sided violence) from the open UCDP download, filtered by country, area, dates and deaths. UCDP candidate data is preliminary and weeks old, and every result says so and names the newest event date. Both need no key. The tool count is now 210.
    - URL downloads use the QGIS network stack: Downloads of web addresses named by the model, including the humanitarian data downloads, now go through QGIS's network stack (your proxy, authentication and SSL settings apply). Every redirect is still checked against the private-address block before it is followed, one hop at a time, and the key is dropped when a redirect leaves the site.
    - Licence notice says GPL version 2 only: The notice in the plugin's root file offered 'any later version'; it now says version 2 only, matching the package metadata and the manifest.
    - Not hand-tested: The earthquake and UCDP data fetches were run against the live services from a development session, and CI runs the rest, including the QGIS 4.2.2 live job, but none of this has been hand-tested in desktop QGIS or with a real model. Whether the assistant picks the new tools for plain requests is not yet known.

v1.16.0-rc34 (2026-10-11):
    Release candidate 34 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Makes a refused Gemini request explain itself, replaces the retired fallback model, and stops waiting on models the plan cannot use.
    - A refused Gemini request now explains itself: When Google refuses a request, the chat message says what each model answered (the status and Google's own text, for example 'Your project has been denied access'), which API key was sent (its length, last four characters and where it was found: QGIS saved credentials, this session's memory or an old plaintext setting) and, when the reply is not a normal Google error, the server name and size of the reply. Long key-like strings in provider text are blanked and the whole key is never shown.
    - Retired Gemini model replaced: Google answers 404 for gemini-2.5-flash ('no longer available to new users, use gemini-3.8-flash'). The fallback list is now gemini-flash-latest, gemini-3.8-flash and gemini-3.7-flash, and Settings offers gemini-3.8-flash.
    - No more waiting on a model your plan cannot use: A quota of zero (the free tier has none for the Pro models) is returned at once instead of after a minute of retries, and the plugin moves to the next model. A model refused with 403 is also skipped. When Google denies the whole project, the plugin stops and shows Google's message instead of trying every model. A Google quota error is shown as one sentence (model, free tier, limit, retry time) instead of a screenful of JSON.
    - Log lines keep their host names: The redirect and refusal log lines had lost their host fields to the logger's allow-list; host names (never paths, keys or bodies) are now logged.
    - Not hand-tested: CI is green, including the live checks in QGIS 4.2.2, but none of this has been hand-tested in desktop QGIS or run against Google. Which key and project Google accepts remains the user's to check in Google AI Studio.

v1.16.0-rc33 (2026-10-11):
    Release candidate 33 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes AI requests losing their API key when a server redirects them, the likely cause of a reported Gemini 403 (not confirmed), and logs redirects without secrets.
    - AI requests keep their API key when a server redirects them: If anything between QGIS and a provider (a proxy, firewall or gateway) answered an AI request with a redirect, QGIS's own sender repeated the request without the Authorization header, so the provider refused it with 403. This was reproduced in QGIS 4.2.2 with a local test server. Requests that carry a body or a key now follow redirects inside the plugin: the same method, headers and body on a 307 or 308, a plain GET on 301, 302 and 303, the key kept on the same site and dropped for another, relative targets resolved, at most 5 hops.
    - Redirects are logged without secrets: Each redirect writes a log line with the two host names and the status, never the path, key or body, so a later refusal shows whether a redirect was involved.
    - Not hand-tested: CI is green, including the live checks in QGIS 4.2.2 against a local test server, but none of this has been hand-tested in desktop QGIS or run against Google, and it has not been confirmed that a redirect is what caused the reported Gemini 403. The reason code and the sender switch added in rc32 remain for that case.

v1.16.0-rc32 (2026-10-11):
    Release candidate 32 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Shows the provider's reason when an AI request is refused, adds a switch back to the previous request sender, and logs refusals without secrets.
    - A refused AI request now says why: When Google, OpenAI or another provider answers 401 or 403, the chat message now includes the provider's own short reason code (for example PERMISSION_DENIED, API_KEY_INVALID or SERVICE_DISABLED). The rest of the response is still hidden, because some providers echo part of the key back, and only values that look like a plain code name are shown.
    - Switch back to the previous request sender: A Gemini key that worked in a test outside QGIS was refused with 403 when the plugin sent the same kind of request through QGIS's network stack. The cause has not been found. Setting cartogen_ai/hosted_requests_via_qgis to false in QGIS's advanced settings sends hosted AI requests (Gemini, OpenAI, Claude, OpenRouter) the way versions before rc28 did. It is on by default.
    - Refusals are logged without secrets: A 401 or 403 on the QGIS path now writes a log line with the host and whether a QGIS proxy is set, never the key, the headers or the response body.
    - Not hand-tested: CI is green, including the live checks in QGIS 4.2.2, but none of this has been hand-tested in desktop QGIS, and it has not been confirmed that the switch fixes the Gemini 403.

v1.16.0-rc31 (2026-10-11):
    Release candidate 31 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Aligns the plugin with the QGIS 4.2 developer cookbook: metadata category, checked temporary layers, in-dialog notices and redraw after field writes.
    - Metadata matches the QGIS plugin rules: The metadata declared a menu category (Plugins) that the QGIS developer documentation does not allow; it is removed, and the plugin stays in the default Plugins menu.
    - Clearer error when QGIS cannot create a temporary layer: 18 places that create a temporary layer now check that QGIS actually created it and report a readable error, instead of failing a line later with a confusing message about a missing data provider.
    - Notices appear inside the dialog: Results such as 'exported', 'deleted' and 'failed' in the Memory, Layer Data Sensitivity and account dialogs now show in a message bar inside the dialog instead of a pop-up box. Questions that need an answer, and the warning when an API key is kept for this session only, still use a pop-up.
    - Styled layers redraw after score fields are written: After the severity, presence-gap, population and index tools write a field into a layer, the layer is redrawn, so a layer styled on that field no longer shows the old colours until the next refresh.
    - Not hand-tested: CI is green, including the live checks in QGIS 4.2.2, but none of this has been hand-tested in desktop QGIS, and the new message bar has been checked only by an automated test, not by eye.

v1.16.0-rc30 (2026-10-11):
    Release candidate 30 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Moves large downloads and the account client onto QGIS's network stack, makes the script fallback fail closed, and adds the directory's upload rules as a test.
    - Large downloads and the account client use QGIS's network stack: Geofabrik extracts, model checkpoints and other large downloads are now read through QGIS's network stack, in chunks, so QGIS proxy, authentication and SSL settings apply, progress and cancel still work, and a download cut short is reported as an error instead of leaving a smaller file. The Cartogen account client does the same. Three things still use Python's own HTTP code: a download that answers with a redirect (it is retried through urllib with the QGIS proxy; following the redirect through QGIS failed in testing and the cause was not found), the check that every redirect of a fetched URL is safe (QGIS follows redirects without letting the plugin look at each step), and Ollama (a local server whose answers can take longer than QGIS's 60 second network timeout).
    - Scripts do not run without isolation: If QGIS cannot be loaded, the script tool now refuses to run a script instead of running it in the plugin's own process.
    - Directory checks reproduced: The plugin directory's own upload rules are now an automated test against the built zip, and the code passes the pyflakes checks (unused re-exports and conditional redefinitions removed).
    - Contact address: The project contact address is now cartogenai@gmail.com.
    - Not hand-tested: CI is green, including the live checks in QGIS 4.2.2 against a local test server, but none of this has been hand-tested in desktop QGIS, and the new download path has not been run against Geofabrik or any real service.

v1.16.0-rc29 (2026-10-11):
    Release candidate 29 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Closes the findings of the 2026-10-11 security and code-quality audit. Replies cannot forge internal links, remote data is size-limited, the package is leaner, and diagnostics go to the QGIS Message Log.
    - Replies can no longer forge internal links: A reply (or a layer attribute it quoted) could draw a look-alike Apply edit link that resolved a real pending confirmation, and a quote in a link address could add attributes to the link. Replies can now create only the four chip links (action, export, prompt, zoom); a quote, angle bracket or space in a link address is encoded; and a file link opens only an existing file of a type the tools write (pdf, png, html, csv and similar).
    - Limits on remote data: Data fetches no longer read an unbounded answer into memory. JSON and CSV answers are capped at 64 MB, downloads at 1.5 GB, and a gzip tile or zip member that would expand past 256 MB is refused instead of inflated.
    - Smaller, cleaner package: The release zip no longer carries hidden files (a build once included a 92 KB .coverage file) or about 70 internal development documents; only the README, user guide, tools reference, humanitarian catalogue and screenshots ship (206 files instead of 288).
    - Diagnostics in the QGIS Message Log: Startup, unload and error messages that were printed to the Python console now go to the QGIS Message Log, with the same redaction as every other log line and no exception text.
    - Not hand-tested: CI is green, including the live checks in QGIS 4.2.2, but none of this has been hand-tested in desktop QGIS, and no real API key has been used through the QGIS network path.

v1.16.0-rc28 (2026-10-10):
    Release candidate 28 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Warns about a leftover pre-rc25 install and tidies documentation and Qt6 names.
    - Warning about an old install: If an install from rc24 or earlier (folder cartogen-ai) is still present next to this one, the plugin now shows a warning when it starts, because both copies would otherwise load together. Delete the old folder and restart QGIS.
    - Documentation and Qt6 tidy-up: The README and user guide no longer tell users to pick OpenAI or Claude, which have not been offered since rc25. The label line-placement flags use their scoped Qgis names, and CI confirms they and the QVariant field types exist in QGIS 4.2.2.
    - Not hand-tested: CI is green, including the live checks in QGIS 4.2.2, but none of this has been hand-tested in desktop QGIS, and no real API key has been used through the new network path.

v1.16.0-rc27 (2026-10-10):
    Release candidate 27 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Sends AI provider requests through QGIS's network stack and scopes the enum names the plugin directory's Qt6 check flagged.
    - AI provider calls use QGIS's network stack: Requests to OpenRouter, Google Gemini and the Cartogen API entry now go through QGIS's own network stack, so QGIS proxy, authentication and SSL settings apply to them. A call waits at most the timeout set in QGIS (Settings > Options > Network, default 60 seconds). Ollama (a local server that can take minutes to answer) and the Cartogen account client still use the requests library. While QGIS's Network Logger panel is open, request headers, including the API key, appear in it on your own machine. This path has been tested in CI against a local server only, not against a real provider.
    - Qt6 enum names: The 38 unscoped enum uses the plugin directory's Qt6 check flagged (for example QgsProcessing.TypeVectorPoint) now use the scoped names, and a live test confirms each resolves in QGIS 4.2.2.
    - Not hand-tested: CI is green, including the live network and enum tests in QGIS 4.2.2, but none of this has been hand-tested in desktop QGIS or run with a real model or API key.

v1.16.0-rc26 (2026-10-10):
    Release candidate 26 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Sends data-fetch requests through QGIS's network stack and fixes the plugin-directory security scan findings.
    - Network requests use QGIS: Data-fetch tools (OpenStreetMap, HDX, hazard feeds, STAC and the like) now send their requests through QGIS's own network stack, so QGIS proxy, authentication and SSL settings apply. QGIS sets the User-Agent and applies its own network timeout (Settings > Options > Network, default 60 seconds) on these calls. Large downloads (Geofabrik extracts, model checkpoints) are still streamed with urllib, using the QGIS proxy. The AI provider clients still use the requests library; moving them is planned (#71).
    - Security scan fixes: Short-key hashes are marked as non-security, a failed restore of the database session's read-only mode is now logged, and the reviewed best-effort handlers carry a reason, so the plugin-directory security scan reports far fewer findings.
    - Not hand-tested: CI is green, including the new live network tests in QGIS 4.2.2, but none of this has been hand-tested in desktop QGIS or run with a real model.

v1.16.0-rc25 (2026-10-10):
    Release candidate 25 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Limits the offered providers to the tested ones and carries the plugin-directory fixes.
    - Providers limited to tested ones: Settings and the quick switcher now offer OpenRouter, Google Gemini, Ollama (local) and the Cartogen API entry only. OpenAI and Claude are no longer listed or run, because they were not tested. A provider saved by an earlier version (for example OpenAI) falls back to OpenRouter and its saved key and settings are left untouched. The Cartogen API entry is a stub: no hosted gateway is deployed yet, so it cannot be used until one exists.
    - Plugin-directory fixes: metadata.txt now parses (no percent sign), gives the project email and GitHub homepage, states requirements and the data sent to a cloud provider, and the package leaves out hidden git files, sample data and binary documents.
    - Opt-in API trace and local layer list: A Settings checkbox (off by default) records every model request and reply to a local file for diagnosing call counts. 'List the layers' is answered in the dock with no model call.
    - Plugin folder renamed: Plugin folder renamed: the zip's top-level folder is now cartogen_ai_plugin (a valid Python identifier, which plugins.qgis.org requires) instead of cartogen-ai. If you installed rc24 or earlier, delete the old cartogen-ai folder from your profile's plugins directory, otherwise two copies load.
    - Not hand-tested: CI is green, but none of this has been hand-tested in desktop QGIS or run with a real model.

v1.16.0-rc24 (2026-10-10):
    Release candidate 24 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Adds checks on replies, a guessed-path guard, background raster runs and read-only database layers.
    - Replies checked against tool results: A reply is footnoted when it names an identifier no tool returned (District_4), gives a total, mean or preliminary figure that matches no tool result, or says a style was applied when no styling call succeeded (#228). These are wording checks, not a fix for the model inventing values.
    - Guessed file paths: After two 'file not found' errors in a row the agent is told once to stop guessing and use a path you gave or a tool returned, or ask you (#231).
    - Less freezing: Approved native Processing algorithms (buffer, clip and the like) now run in the background with Stop available, except ones that change your own layer. The raster tools use GDAL algorithms, which crashed the CI job when run on a worker thread, so they and contours, task-grid statistics and some logistics steps still run on the main thread (#221 is only partly done).
    - Safer scripts and SQL: Database and web layers are read-only inside the script worker (#220), and the result layer of execute_read_only_sql cannot be edited (#151). A read-only database role is still the real guarantee for SQL.
    - Not hand-tested: CI is green, but none of this has been hand-tested in desktop QGIS or run with a real model.

v1.16.0-rc23 (2026-10-09):
    Release candidate 23 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Adds cost measurement, routing, new tools, multi-step chains and an evidence folder, and fixes from the hand test of rc22.
    - Cost: every model call is measured (provider-reported input, cached and output tokens, latency, tools offered; estimates are labelled and kept apart) and each reply's footer shows what the request used. A loop guard stops a turn that repeats an identical call, fails three times in a row or makes no progress, and says what was done. A per-turn token budget (default 450,000, a provisional figure taken from the rc22 hand-test logs; 0 = no limit; Settings) stops a request BEFORE a call that would pass it and reports what finished.
    - Routing: the tools a request needs are chosen from the data sources and actions it names (OpenStreetMap, HDX, buffer, clip, slope, contours and so on) instead of weak word matches, with find_tools for a step the offered list did not cover. A task that names one hazard (drought, extreme heat) only matches when the request names it. Several separate requests pasted together are shown as a queue and run one at a time after you confirm.
    - New tools: split_lines_by_zones (a road or highway split by exclusion zones, with total passable length AND longest continuous passable segment, ellipsoidal), generate_contours, and fetch_dem (Copernicus DEM GLO-30, about 30 m, no account needed; a surface model, not bare earth; the result records source, tiles, date and datum and returns the licence's required notices). Print layouts and PDFs are NOT stamped with the Copernicus notice: add it yourself when you publish.
    - Fewer model round trips: run_steps lets the model send up to 8 tool calls in one step, checked against the open project before anything runs (layers exist and are the right kind and geometry, no buffering a geographic-CRS layer, numeric style fields) and stopped at the first error or pending confirmation. Eleven vetted chains (exclusion-zone split, buffer then clip, coverage gap, outside distance, terrain contours, terrain slope, clip DEM then slope, clip-style-zoom, reproject then export, population exposure, admin boundary validation) are offered when a request names all their steps, with a project check that binds layers and suggests the UTM zone. The coverage-gap chain is a straight-line distance gap only, not a service gap.
    - Evidence (off by default, Settings): each request can write a folder with every tool call's real arguments, results, timings and layers created, copies of written files with SHA-256, and a canvas screenshot. It stores raw values, unlike the logs, so check it before sharing.
    - Fixes from the rc22 hand test: a relative output path is anchored to the saved project's folder and the reply names the full path; export_layer writes a GeoTIFF for a raster; a print layout map follows the canvas rotation; 'show the severity on the map' no longer asks which hazard; after a confirmed Apply edit the 'show it on the map' part runs; a project change drops the previous plan so a plain 'Yes' cannot resume it; apply_graduated_style accepts color_from and color_to; 'cluster ... into N groups' reaches unsupervised classification.
    - Fixes after that: a natural 'Yes, proceed.' at the preview now runs the original request; when a tool hands over its map hint and the request asked to see the result, the agent asks once for the styling call, and point layers are lifted above polygon layers that cover them; a layout built over JIAF-support results carries the 'not the JIAF method, not endorsed by OCHA or the IASC' statement; people-in-need legends say 'people' and a value exactly on a class limit now draws in the class its label gives.
    - Verification: GitHub Actions ran green on the merged head (Ubuntu with Python 3.10 and 3.11, Windows, the QGIS 4.2.2 live job with PostGIS, and the gitleaks scan), and the live tests reproduced the preview-reply fix before fixing it. NOT hand-tested in desktop QGIS: every item above, and no real model has yet run run_steps, the chains or the request queue. Open and not fixed: invented or wrong facts in replies (model behaviour, issue 228), reply-wording requirements (issue 229), legend entries hidden under opaque polygons (issue 165). No issue is closed by this build.

v1.16.0-rc22 (2026-10-07):
    Release candidate 22 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes from the rc20 architectural audit.
    - Fix: a result that replaces a layer of the same name no longer overwrites a layer you made yourself. Plugin results are marked as the plugin's own; a same-named layer of yours is set aside (renamed) and the reply says so (rc20 audit A02).
    - Fix: feature extraction from imagery only outlines the detected object, not the surrounding background (A01), and a rotated or non-square mask keeps its true geometry (A10).
    - Fix: a long-running agent turn that was stopped or superseded can no longer deliver a stale result or act after the project changed (A04); the data-egress gate now follows layer lineage through renames and duplicates, so a derived layer keeps the protection of its source (A05); feature edits report a refused commit or delete instead of claiming success (A03).
    - Fix: histogram equalisation no longer turns the lowest valid pixels into NoData: valid cells are 1-255 and 0 is NoData only (A09). Slope and hillshade on a projected DEM in feet convert metres into the DEM's own units (A11). GeoJSON export is RFC 7946: WGS84 coordinates, no legacy crs member (A12). A failed CRS transform is now an error instead of zooming or building a layout in the wrong place (A13).
    - Safety: the isolated script worker (execute_pyqgis_script) receives COPIES of file-backed layers, so a script cannot modify your files; edits it makes to such a layer are discarded. Database and web-service layers have no file to copy and remain writable by a script (A06, issue 220).
    - Memory: classification refuses rasters over 25 million cells or 60 million cell-band values (A15). Abandoned background-analysis objects are released once their task finishes (A16). The plugin's last-resort import bootstrap removes only its own modules (A18).
    - Responsiveness: zonal_statistics (zones of 200 or more) and the model and outline steps of extract_features_from_imagery run off the GUI thread and honour Stop. Every other expensive tool still blocks QGIS while it runs (A14, issue 221 stays open).
    - Verification: the full local run passed on the audit-fix build (offline suite, ruff, zip check, 360 QGIS 4.2.2 live tests against PostGIS). NOT hand-tested: every fix above. Imagery extraction needs the FastSAM model and was not run live. GitHub Actions did not run (account billing), so the Windows job and the gitleaks scan did not run. No audit issue (220-224) is closed by this build.

v1.16.0-rc21 (2026-10-07):
    Release candidate 21 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes from the rc20 hand test.
    - Fix: manual class boundaries passed to apply_graduated_style must be finite, distinct numbers inside the field's own range. A boundary of 100 for values 5-15 used to make an inverted "100 - 15" class and a legend that matched nothing (rc20 hand test, #165); it is now an error that names the data range.
    - Fix: the north arrow on a print layout is linked to the map, so it turns with a rotated map. In rc20 it stayed at 0 degrees on a map rotated 30 degrees (rc20 hand test, #165). Checked in real QGIS 4.2.2: the new test fails on the old code and passes on this one.
    - Fix: a short pasted fragment shaped like key: value (for example "template: access_map") can no longer make the agent create, change, delete or export anything. In the rc20 hand test a real model called get_layers and create_print_layout for that fragment alone and left a layout behind (#130). The agent may still read the project and ask what you want. Natural short replies ("yes", a layer name, "EPSG:3857" after a question) and any turn with a confirmation card pending are not affected.
    - Dev: tools/local_ci.sh runs what the CI workflow runs on a developer machine (offline suite, byte-compile, ruff, release zip check, and the QGIS 4.2.2 live job with PostGIS in Docker). It does not cover the Windows job or the secret scan.
    - Verification: the full local run passed (3,669 offline tests, ruff, zip check, 345 QGIS 4.2.2 live tests against a real PostGIS server). NOT hand-tested: every fix above. The stray-fragment gate was tested with fakes at the tool-dispatch layer, not against a real hosted model. GitHub Actions did not run for this build (account billing), so the Windows job and the gitleaks scan did not run. No audit issue is closed by this build.

v1.16.0-rc20 (2026-10-06):
    Release candidate 20 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Audit fixes and CI coverage since rc19.
    - Fix (rc19 regression): a PostGIS read-only query left the shared QGIS database connection read-only for everything else that used it, because the guard set the session read-only and never put it back. It now reads the previous mode, runs the guard and restores it on every path. Found by the first CI run against a real PostGIS server (#151).
    - Fix: raster arithmetic on mismatched grids (NDVI, NDWI, NDRE, weighted overlay, change detection) warps the other rasters onto the first raster's grid into temporary files and says so; project layers are not touched (#153). DEM vertical unit is an argument ('m', 'ft', 'us_ft') on elevation_profile, slope_analysis and build_composite_impedance_field (#154). A population raster named like a density raster is refused until raster_unit says people_per_km2 or people_per_cell (#160).
    - Fix: severity, presence-gap, population-in-need and damage-exposure tools label units uniquely and write each score to its own feature instead of collapsing same-named units (#159); incident and footprint points are moved into the admin CRS before assignment (#161).
    - Fix: a task or function that finishes after the plugin was unloaded no longer calls back into the destroyed UI (#167). The standard print-layout legend lists only layers that meet the map, and right and top graticule labels that were clipped by the legend frame are hidden (rc18 hand test R7/N8).
    - Fix: renaming a protected layer, or giving an open layer the same name, no longer lets a layer derived from it look open to the cloud-egress gate. Lineage now also records source layer ids, and a name shared by several layers takes the strictest level (#150).
    - Fix: scheduling a stored workflow (schedule_recurring_workflow) is treated like running one: with a cloud provider it needs the same override, because each tick posts results into the chat the model reads next (#149).
    - Fix: estimate_road_speeds, add_incident_point, add_named_points and the NASA FIRMS, EONET and GDACS layer refresh reported success when the provider refused the save, and committed edits you had pending on the same layer. A refused save is now an error that keeps the previous contents, and a layer you are editing stays in your own edit session (#144, #143).
    - Fix: a roads layer with a few segments that have no direction (NULL or empty) was no longer recognised as the Geofabrik F/T/B encoding, so every one-way street was routed as two-way. Blank directions are ignored when the encoding is detected (#132). Two dashboards with one title in one second no longer overwrite each other (#158).
    - CI: the live job now starts a PostGIS 16 server and runs the read-only SQL tool against it, plus new live tests for edit-session undo, project switching, lineage, admin-boundary styling and the population ramp.
    - Verification: offline suite, ruff and the QGIS 4.2.2 live job (including PostGIS) pass. NOT hand-tested: every fix above. The rc19 sheet's rows V, T, J and P and the rc18 hand-test findings are still unverified in a desktop session. No audit issue is closed by this build.

v1.16.0-rc19 (2026-10-06):
    Release candidate 19 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes from the rc18 hand test.
    - Fix: a polite yes now confirms a task card ("Yes, proceed.", "Yes please", "ok go ahead"). In rc18 only a bare "yes" did, so the reply went to the model as a new message and the card was lost (rc18 hand test R2). A reply with extra content ("yes but use the roads layer") is still an edit, and destructive confirmations keep their strict wording.
    - Fix: the imagery model checkpoint is downloaded with a plain whole-file request into the weights folder (no Range header, so HTTP 416 cannot happen; a short file is never accepted) before the ultralytics downloader is tried (rc18 R3 failed with 416 after 357 s). The real asset was checked: a plain request returns it complete (23,851,578 bytes).
    - Fix: after you press Stop, or when a tool failed in the turn, no follow-up turn asks the model for a report you did not request. "Report the model used and the count" (report as a verb) no longer makes a report file the deliverable; "write a report" still does (rc18 R3, N5).
    - Fix: task matching. A request that names a loaded raster no longer gets "imagery = most recent low-cloud scene" assumed, and a tool you name leads the suggested tool chain.
    - New: get_layer_extent returns a layer's (or the map view's) extent in its own CRS and in WGS84, with the box in both orders named (south-west-north-east for OSM and building footprints, west-south-east-north for the STAC search). A prompt rule tells the model never to write a bounding box from memory. In rc18 the model passed a Damascus box for a fixture in Jordan to the STAC and building-footprint tools (N4, N5).
    - Fix: a result drawn by apply_humanitarian_look is lifted above raster layers that were hiding it (rc18 N2), and the Layers panel legend is refreshed after a raster colour ramp (rc18 R4). The layer is only moved, never hidden or deleted; a layer inside a group is left alone.
    - Fix: a web link inside a table cell (a STAC thumbnail) is no longer turned into a broken file link by the output-path matcher (rc18 N4).
    - Docs: the rc18 live smoke report as received, with its triage (docs/RC18_LIVE_SMOKE_REPORT_2026-10-06.md, docs/RC18_SMOKE_TRIAGE_2026-10-06.md) and a re-test sheet for this build (docs/SMOKE_RUN_SHEET_rc19_2026-10-06.md).
    - Verification: offline suite and ruff pass. NOT hand-tested: every fix above. NOT run in CI at the time of writing: the new live tests (renderer looks, lift above rasters). Rows V, T, J and P of the rc18 sheet were not executed in the rc18 hand test, so the new JIAF, IPC/INFORM/UNOSAT and plain-language changes are still unverified in a desktop session. No audit issue is closed by this build.

v1.16.0-rc18 (2026-10-06):
    Release candidate 18 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes from the rc15 and rc17 installed-profile smoke reports (PRs #204-#206).
    - Fix: a fresh full request typed while a clarification question is open no longer gets pasted under the old request as "Details: ...", which ran the old request with the new one underneath. This was behind the wrong clarification questions, previews and the unrelated OpenStreetMap offer in the rc15/rc17 smoke runs. Short answers behave as before.
    - Fix: after you click Apply edit on a confirmation card, a request with more than one step ("write the score, then style the layer") resumes once, up to three times per request. Before, it stopped after the confirmed step.
    - Fix: a short follow-up such as "EPSG:3857" keeps the tools the previous turn used (at most six, for two turns), so the CRS question can finally be answered with the point being placed (the rc16 retry could not work because the tool was not in the turn's tool list).
    - Fix: the reply guard no longer reads metres as millions ("886.49 m" was footnoted as general knowledge), and no longer deletes a table whose rows are found in the tool results just because an unrelated call failed.
    - Fix: task matching. A request that names a tool is only matched to a task that uses it; a weak cross-section tie (under 25 percent keyword coverage) and a single shared word in a long request give no directive; a request that names a loaded layer is not asked "Which facility or service type?"; hub, depot, site and similar words answer that question. The register's own task descriptions match as often as before (453 and 466 of 748, pinned by a test).
    - Fix: exporting over a non-empty file the plugin did not write (or one changed since) now asks for confirmation (export_to_csv, export_layer). Hub siting, allocation, route stops and travel-time origins label results with the layer's name field instead of the GeoPackage id. save_project lists temporary layers that come back empty. Raster colour ramps keep their range after a save. buffer_analysis accepts output_name.
    - Fix: extract_features_from_imagery downloads its model checkpoint on a background thread (QGIS no longer stops responding for the download), removes a partial file after a failure and retries once, and never prints the signed download URL. Web searches for the latest or current thing carry today's date; script-sandbox "Algorithm not found" errors name the real tools; the agent is nudged once when a tool you named is still not called after four other calls.
    - New: plain-language styling: colour words such as "dark red" or "pale yellow" are accepted, and everyday phrasing reaches the matching style tool (routing only; readability is measured in the run sheet, not claimed).
    - New: JIAF 2 results can be drawn: phase 1-5 severity (a unit with no phase shown grey as not assessed), review status (units pending the group's decision) and counts. Not hand-tested; support for the process, not the JIAF method.
    - New: results of IPC/INFORM/UNOSAT imports, JIAF inputs, zonal statistics, road speeds, impedance and unsupervised classification are now drawn readably instead of default colours (not hand-tested; some tools still draw nothing, see the changelog).
    - Docs: the rc15 and rc17 live smoke reports as received, their triage, and a re-test sheet for this build (docs/SMOKE_RUN_SHEET_rc18_2026-10-06.md).
    - Verification: offline suite and ruff pass; the live-QGIS tests passed in CI on QGIS 4.2.2 (PRs #204-#206). NOT hand-tested: the chat flow, task matching and the CRS retry in a real conversation, the continuation after Apply edit, and the imagery download with a real model. No audit issue is closed by this build.

v1.16.0-rc17 (2026-10-06):
    Release candidate 17 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes from the rc15 installed-profile smoke report (PR #204).
    - Fix: a request that names a tool ("Use optimal_hub_siting ...", "Run extract_features_from_imagery ...") is only matched to a registered task that uses that tool; otherwise it is sent as typed. In rc15 such requests were matched to unrelated OpenStreetMap export or download tasks and the injected tool list steered the model away (smoke report D01).
    - Fix: hub siting, location allocation, route stops and travel-time origins label their results with a name-like field (name, label, title, *_name) instead of the first attribute. On a GeoPackage the first attribute is the primary key, so hubs came back as "1", "2", "3" and the model had to guess which was which (D03). The distances themselves were already correct.
    - Fix: save_project lists temporary (memory) layers and warns that they come back empty when the project is reopened (D07).
    - Fix: pseudo-colour rasters record their stretch range on the renderer; saved projects had classificationMin/Max = nan and the reopened legend read nan (D08).
    - Fix: a failed FastSAM model download no longer prints the signed download URL and says what to do about a partial checkpoint (HTTP 416) (D06). The download still blocks QGIS while it runs; that is not fixed.
    - Docs: the rc15 installed-profile smoke report as received and a per-defect triage (docs/RC15_LIVE_SMOKE_REPORT_2026-10-06.md, docs/RC15_SMOKE_TRIAGE_2026-10-06.md). Still open there: no follow-up model turn after Apply edit (D05, needs a decision), the main-thread model download, requests in a cross-section tie that name no tool, D09 and D10.
    - Verification: offline suite and ruff pass; the live-QGIS tests passed in CI on QGIS 4.2.2 (PR #204), including new live tests for the raster range and the hub-siting labels and distances. Model-dependent behaviour (task matching in a real chat, the CRS retry from rc16) and the print layout have NOT been hand-tested; no audit issue is closed by this build.

v1.16.0-rc16 (2026-10-06):
    Release candidate 16 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes found in the rc15 hand test (PR #202).
    - Fix: list-returning tools (get_layers, get_attributes) were reported as "Execution failed unexpectedly." since rc12 (a dict-only check in the main-thread wrapper added with audit item #138). Found in the rc15 hand test; they return their results again, confirmed by a new live test in CI on QGIS 4.2.2.
    - Fix: when the CRS question for point coordinates (#119) was answered, the model made no tool call and pasted a script instead. The "ask" result now says how to retry and the prompt rule spells it out. Depends on the model; needs a hand re-run.
    - Fix: a print layout zoomed to a one-point layer had a zero-size extent ("Scale unavailable", "Invalid scale!", a map stuck on "Rendering map"). The extent is padded around the point (about 2 km, a guess, not a measured value); an empty layer keeps the old fallback. Needs a hand re-run.
    - Internal: one tool-result contract (core/agent/tool_result.py) replaces scattered dict checks, and the CI live-test job discovers tests/test_*_live.py by name (a module that does not run must be listed with a reason). test_agent_live is excluded with its reason stated.
    - Docs: a blank run sheet for the live smoke test (docs/SMOKE_RUN_SHEET_rc15_2026-10-05.md).
    - Verification: offline suite and ruff pass; the live-QGIS tests passed in CI on QGIS 4.2.2 (PR #202). The B1 retry and the layout fix have NOT been hand-tested; no audit issue is closed by this build.

v1.16.0-rc15 (2026-10-05):
    Release candidate 15 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Adds the replace-by-name warning (PR #199) on top of rc14.
    - New: when a service-area run replaces a same-named result layer that was built with different parameters (strategy, travel cost, speed or direction field, default speed, road layer), the tool result lists what changed and the reply must say so (issue #130, point 3). An identical re-run and layers made before this change stay silent. It reports after the replacement and does not ask first; only the service-area layers record their parameters so far (optimize_delivery_route and the access reach polygons are unchanged).
    - Docs: a per-issue hand-verification checklist (docs/ISSUE_VERIFICATION_CHECKLIST_2026-10-05.md) and a corrected README audit-status line (#151 needs a database; #152-#168 addressed in work packages 1-6, some with parts still open; all 32 audit issues stay open until hand-verified).
    - Unchanged from rc14: the JIAF 2 analysis-support tools. Flag-formula comparison still awaits the owner's closure and the Annex 4 reader is still an unsupported optional format; do not describe the result as a faithful or complete JIAF implementation.
    - Verification: offline suite and ruff pass; the QGIS-side code ran in CI on QGIS 4.2.2, including the new live test for the replacement warning. Nothing in rc15 has been hand-tested in a desktop session, and no audit issue is closed by this build.

v1.16.0-rc14 (2026-10-05):
    Release candidate 14 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Adds the JIAF 2 analysis-support tools (PRs #192-#195).
    - New: JIAF 2 analysis support (HX5), eight tools -- import_jiaf_inputs, record_jiaf_setup, get_jiaf_setup, compute_jiaf_preliminary, record_jiaf_decisions, get_jiaf_decisions, finalize_jiaf_results, compute_jiaf_patterns. Support for people running the JIAF 2 process: NOT the JIAF method, not endorsed by OCHA or the IASC, and nothing is a final figure. They read sector inputs (OCHA Worksheet 3A/3B read through its Excel tables, HXL tables), compute the preliminary joint PiN (highest sector, never summed or averaged), preliminary severity and the PiN and severity flags, record the multi-partner group's decisions in the project, and keep the preliminary result, review status, final result and justification apart. Intersectoral severity is per unit; there is no national severity.
    - New: the flag rules are OCHA's own worksheet formulas (profile ocha_worksheet_2026, read from the official example and template workbooks): ranks of distinct values, a tie for the highest switches flags 2 and 3 off, flag 1 at two missing-or-zero sectors, flag 6 in two parts, and the worksheet's rule that the preliminary PiN is the highest PiN only where severity is above 2. Thresholds come from the workbook's Thresholds cells, else the template defaults, and every result says which. The older reading of the manual is kept as profile manual_reading, comparison only.
    - New: incomplete sector coverage is never turned into phase 1 (the unit has no preliminary severity, with lower and upper bounds); missing and zero PiN stay distinct; bulk closure of flags needs a recorded rationale; published final values are shown beside, never over, the calculated ones; blank outcome evidence is not assessable, not passed.
    - Change: the Annex 4 sector-template reader is an explicit UNSUPPORTED optional format (implemented from the manual's screenshots, never auto-detected, compatibility unverified).
    - Not closed: flag 6 on real output, a zero third-highest PiN in flag 3 (from the formula text, no cached example row), the header/formula mismatch of severity flag 4, a real country's thresholds, and the Yemen analysis team's own flag decisions. Do not describe the result as a faithful or complete JIAF implementation.
    - Verification: offline suite and ruff pass; the QGIS-side code ran in CI on QGIS 4.2.2 (live tests written without a local QGIS). The check against OCHA's example workbook (all flag columns, severity and preliminary PiN of its 6 units) and against the Yemen worksheet was run locally; those workbooks are not committed. Nothing in rc14 has been hand-tested in a desktop session, and no audit issue is closed by this build.

v1.16.0-rc13 (2026-10-04):
    Release candidate 13 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Adds the humanitarian work done after rc12 (PRs #183-#189).
    - New: automatic humanitarian map looks (HX1) -- apply_humanitarian_look draws severity (five classes matching the tool's own), people-in-need / exposure (quantile classes, zero its own class), presence-gap and rank results; the analysis tools return a map_look hint; footprints, OSM layers, roads and detected features get humanitarian styles; a situation-report layout.
    - Change: the humanitarian task register was corrected (HX2) -- the new H1-H6 tools are attached to the tasks they serve, and a coverage test keeps every non-guidance task tied to a tool that can acquire its data.
    - New: calculate_allocation_envelope (HX3a) -- splits a budget you supply over areas by need (optionally x population) with ceiling, floor, need threshold and rounding; areas with missing values are excluded and listed, never imputed; an advisory calculation, not a recommendation of who should receive what.
    - New: import_humanitarian_table (HX3b) -- file-based import and validation of IPC phase, INFORM Risk and UNOSAT damage tables with a P-code join to an admin layer. UNVERIFIED: the recognised column names were not checked against real HDX files; the result shows the mapping used and an explicit mapping overrides it.
    - New: analyze_critical_links (HX3c) -- screens a road network for bottlenecks (how much origin-to-destination demand has its shortest path through each segment). A screening of dependence, not a closure simulation or traffic forecast. Performance on a national network has not been measured.
    - Change: the in-plugin Help now shows what's new in this version and lists the humanitarian tools by workflow, with tool and task counts read from the live registries; the README, user guide and Help are kept in step with each version by a test.
    - Verification: offline suite and ruff pass; the QGIS-side code ran in CI on QGIS 4.2.2 (live tests written without a local QGIS). Nothing in rc13 has been hand-tested in a desktop session, and no audit issue is closed by this build.

v1.16.0-rc12 (2026-10-02):
    Release candidate 12 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes from the first full hands-on smoke test of rc11 (Yemen / Sanaa, QGIS 4.2.2). Each fix has tests and the QGIS-side ones ran in CI on QGIS 4.2.2, but none has been re-run by hand yet.
    - Fix: an unstated projected CRS that is not the project CRS is asked about instead of guessed (the point was placed silently before); a single point that already exists in a small point layer reuses that layer instead of stacking a new origin layer and result set on every repeat.
    - Fix: the chat follows new content only until you scroll up (a tall confirmation card used to leave the view stuck); chat bubbles show a clock time instead of a frozen "just now"; a degree written as ^\circ no longer shows raw LaTeX.
    - Fix: the no-basemap HTML dashboard draws its markers (the map had no maxZoom, so the cluster library threw).
    - Fix: a downloaded admin-boundary layer is labelled (adm1_name was not a label field) and drawn as a pale backdrop under the analysis; a population raster named yem_ppp_2020 keeps its ramp instead of going opaque black.
    - Fix: print-layout graticule is thin and pale with small whole-number annotations; the legend uses the non-deprecated sync call.
    - Fix: a two-stop route runs one shortest-path search instead of three (a 14-minute request); the route layer draws above the road network; the route step now receives the one-way and speed parameters; the reply says "shortest route", not "fastest". Geofabrik one-way codes F/T/B are recognised (they were ignored). estimate_road_speeds gives a clear error when a layer will not accept the new field.
    - Change: "within reach" facilities are blue instead of teal.
    - New: apply_network_barriers (blocked or slowed roads near destroyed bridges, checkpoints or flood extents, for the speed_field of the routing tools), evaluate_forecast_trigger (applies your own threshold, lead time and probability to forecast values in a layer), generate_mapping_task_grid (mapping task grid for remote mapping, with optional priority and GeoJSON export), design_sampling_frame (survey sample sizes per stratum and a reproducible draw; the statistical assumptions are shown and the design effect must come from the survey designer), calculate_mcda_ranking (weighted multi-criteria ranking of areas with a check of how much the ranking depends on the weights), aggregate_survey_indicator (weighted survey shares and means per area; groups with too few respondents are suppressed). Map looks for the task grid, barrier segments, sample points, hazard alerts and before/after differences; a humanitarian tools catalogue in the docs. Tested offline and written for CI on QGIS 4.2.2; not tried by hand.
    - Fix (architectural audit P1 findings F01-F15, issues #137-#151): arbitrary Processing parameters no longer allow code execution (FORMULA/EXTRA blocked, gdal:rastercalculator removed); field-writing tools use owned, checked edit sessions and never commit a layer you are editing; area/length are SI units regardless of project units; distances are always ellipsoidal; hub siting and service areas handle CRS and multipart inputs; script-isolation results are adopted without losing edits; the cloud egress gate and layer lineage see SQL, workflow and list/nested layer arguments; a running request is cancelled and writes nothing when you switch project; tool state capture runs on the main thread; execute_read_only_sql fails closed. Offline tests plus CI on QGIS 4.2.2 for the QGIS-side code; none of it re-checked by hand, and the project-switch, main-thread and PostGIS changes have only offline fakes behind them.
    - Fix (audit findings #152-#168, second round): roads blocked by apply_network_barriers or given passability 0 are now removed from the network by the service-area, travel-time-matrix and delivery-route tools (closed = negative speed; 0 or empty still means unset, so regenerate speed fields written by earlier builds); the native service-area algorithm's Fastest cost is in seconds (it was 3,600 times too large) and its facilities are moved into the network CRS; estimate_population_exposure leaves your layer alone and gives one total per zone; duplicate or empty names no longer collapse features; boundary points are counted (lowest feature id wins, with matched/ambiguous/unmatched shown); raster arithmetic refuses rasters that are not on one pixel grid; elevation, slope and minimum-area use metres; the large travel-time matrix charges partial-edge costs and both matrix paths use feature ids; a failing layout re-run keeps the old layout and atlas pages no longer overwrite each other; unload removes the translator and the script worker; histogram equalisation and unsupervised classification run without the SAGA/GDAL tools QGIS 4.2.2 lacks; Python 3.10 is the declared floor. New: a note in Settings and the sensitivity dialog saying what a cloud provider always sees of a layer. Offline tests plus CI on QGIS 4.2.2; none of it re-checked by hand (checks: docs/RC12_LIVE_TEST_AND_AUDIT_PLAN_2026-10-04.md section I).
    - Fix: classify_facilities_timing now actually logs its phase times (the logger dropped the fields).
    - Fix: only one confirmation card is live at a time (a newer request supersedes older previews); a casual "yes"/"ok" no longer approves a preview (it gets a hint, no model call); a typed word can no longer approve a one-time cloud override (the button must be used); an unrelated message expires pending previews; the executed/failed result is shown readably (#124, #125).
    - Fix: a short pasted fragment without an action word ("template: access_map", "threshold = 5 km") no longer matches an analysis task; the router hints classify_facilities_by_access ahead of travel_time_matrix for facility-reach requests; re-styling the access layer's access_class keeps the built-in colours (#122, #130).
    - Fix: "population within the service area" now also returns the concave-hull and road-buffer figures with the concave hull as the headline and the convex hull labelled an upper bound (#123); re-downloading an admin boundary replaces the earlier layer of the same name instead of stacking a second one (#127); a layer whose names are all placeholders ("Point", "Origin Point") is no longer auto-labelled (#120); the print-layout body panel is sized to its text and the access-map guide explains the service-area polygon (#129).
    - Fix (QGIS 4.2.2 audit): calculate_damage_exposure_severity no longer fails with ImportError (QgsZonalStatistics is imported from qgis.analysis); hotspot_analysis asks the Processing registry for the kernel-density algorithm id instead of assuming one (the replacement id is unconfirmed); a stale build/ copy can no longer ride into the release zip.
    - New: optimize_delivery_route takes strategy='fastest' (with default_speed or the layer's speed_field) and reports an ESTIMATED travel time plus the route length; the default stays 'shortest' and says so (#131).
    - New: tidy_project_layers finds and, with apply=true, repairs clutter in projects made by older builds: extra copies of the same layer are hidden (never deleted), a black WorldPop raster gets the population ramp, scratch layers are saved into the results GeoPackage (#120, #128). The access-map legend no longer filters by map extent, which was the one step that could drop a visible reach polygon (#129; the exact cause was not reproduced from code).
    - New: a footnote when an answer, after a data tool ran, contains claims no tool result, project summary or user message supports: a named place such as "Amran Governorate", a million-plus or "29.8M" total, a file size such as "240 MB", or terrain/road-surface wording such as "rugged" or "unpaved". The answer itself is never rewritten, rounding and sums/differences of returned figures are accepted, and a short prompt line asks the model to label background knowledge (#75). It is a heuristic over three claim shapes, not a fact checker.
    - New: hydrology engineering tools -- parse_dms_location (DMS to decimal degrees in code), assess_watershed_hydrology_request (refuses to go on until a DEM, a local IDF source and a runoff coefficient are explicit) and calculate_rational_watershed_peak_flow (slope, Kirpich Tc and Rational Method discharge from supplied numbers, labelled preliminary) -- plus task 36.01 and prompt rule 53 so a coordinate-only request is no longer answered with invented watershed figures. The plugin does NOT delineate a watershed, and none of this has run in a live QGIS session.
    - Not fixed: the QGIS-side parts of the three entries above ran in CI only, not in a desktop session; a project with duplicates must be tidied by asking for it (nothing runs automatically).
    v1.16.0-rc11 (2026-10-01):
    Release candidate 11 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes from the first hands-on smoke test of rc10 (Yemen / Sanaa, QGIS 4.2.2); each fix has tests, the QGIS-side ones were run in CI on QGIS 4.2.2, and none has been re-run by hand yet.
    - Fix: a re-saved API key now updates the stored credential instead of failing with "pre-defined config ID is not unique" and falling back to session-only storage.
    - New: the panel opens when QGIS starts (Settings > "Open the Cartogen AI panel when QGIS starts", on by default). A delayed chat-scroll callback no longer crashes when the panel is already destroyed.
    - Fix: CSV export of a point layer now writes X/Y columns (the writer option was never applied). Analysis outputs are drawn above the layer they came from, a data raster above a web basemap, and a layer reorder keeps each layer's visibility; the classified facilities layer is shown on top.
    - Fix: the whole-country WorldPop raster is streamed into <project>/data/00_raw/worldpop and reused (it was read into memory and written to a temp file); a server without range support gets an error with the real size and "do not retry". The Geofabrik extract size falls back to a ranged request and says "size unknown" instead of "0 MB".
    - Fix: a clarification question is asked at most twice, and a coordinate or "the point/origin" answers the facility question; the word "download" no longer forces an exported GPKG/CSV; a drive-time or service-area request is no longer matched to an unrelated task.
    - Fix: a population total summed inside a service-area convex hull is labelled an upper bound; a WorldPop population layer keeps its ramp (empty cells transparent, people/cell legend) when the model asks for a generic stretch; the band-statistics deprecation warning is gone.
    - Fix: re-running an analysis drops the older timestamped tables of the same output from cartogen_results.gpkg (each repeat added ~5 MB).
    - Not fixed: the model's reply still adds claims no tool returned (place names, national totals, terrain); classify_facilities_by_access took 151 s on 3,369 facilities and now logs a per-phase timing line; the orange outline seen on the cost-banded roads is unexplained; rc10's raster legend, labels and print layouts have not been looked at on a real map.
    v1.16.0-rc10 (2026-10-01):
    Release candidate 10 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Includes everything in rc9 plus the visualization round; all QGIS-side behaviour was run in CI against QGIS 4.2.2, nothing yet in a hands-on smoke test, and nobody has looked at a rendered map or exported page.
    - New: service-area roads are drawn in travel-cost bands, the reach figures are distinct translucent polygons grouped under one "Reach figures" group with the headline visible, and unreachable facilities are red, larger and on top.
    - New: WorldPop, density and interpolated rasters get pseudocolour ramps (zero cells transparent) with a unit on the legend; tabular points get one consistent look; Processing outputs are styled by what the algorithm made; small named layers (facilities, admin areas, access points) are labelled automatically.
    - New: create_print_layout styles the page (masthead, type scale, framed panels, a legend of only the visible layers, preparation date, classification prefix) and has template="access_map" for service-area results; the masthead colour is a Settings field.
    - New: ten more allowlisted Processing algorithms (count points in polygon, create grid, extract by location, statistics by categories, DBSCAN and k-means clustering, raster sampling, zonal statistics, reclassify by table, cell statistics).
    - Fix: raster Processing algorithms (slope, aspect, hillshade, IDW, TIN, kernel density, raster calculator) previously failed or reported no output; they now load as layers. A layout with no map extent no longer loses its CRS/scale line.
    v1.16.0-rc9 (2026-10-01):
    Release candidate 9 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Follow-up to the rc8 audit; QGIS-side behaviour was run in CI against QGIS 4.2.2, not yet in a hands-on smoke test.
    - Fix: the reply guard also flags invented records given as bullets or prose and answers in turns where no data tool ran; ungrounded tables are replaced by a notice; the model's own "please confirm" text is dropped when the real confirmation card is shown (F03, F14).
    - New: per-turn tool-call cap and optional token budget, a per-turn token figure in the chat footer, and the limits plus the download-size threshold in Settings; the startup log states which gates are on (F10, F18, F22).
    - New: geoBoundaries, HDX and building-footprint downloads ask first above the size threshold (default 50 MB); a metered connection also asks (F22).
    - Change: population_access_gap reports three labelled figures (concave hull headline, road buffer, convex hull upper bound) and their range (F09). travel_time_matrix uses one shortest-path tree per origin for more than 200 destinations (F08).
    - New: analysis outputs of a saved project are written to data/20_processed/cartogen_results.gpkg and survive Save and Reopen (F06); rc7 layer-tree duplicates are repaired on load (F07); a whole-country WorldPop file is cached and clipped locally when the remote window read fails and the user agreed to the download (F19).
    - New: protected layers' field names are withheld from a cloud model under an enforcing gate (F21); dashboards accept basemap 'none' and a backdrop_layer, and default to the HOT basemap because Carto now needs a key (F12).
    - Fix: LaTeX in replies is shown as symbols, a layer name with parentheses no longer leaves a stray ")" after an action chip, "an analysis", "no route" messages are no longer logged as CRITICAL (F15).
    - Not changed: the chat transcript is still stored in the project file (F25); the 43-minute routing request was not profiled; WorldPop windowed reads, the Excel CSV and a dashboard opened from disk still need a hands-on check.
    v1.16.0-rc8 (2026-09-30):
    Release candidate 8 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Fixes from the rc7 interactive Yemen smoke test (findings F01-F25, GitHub issues #72-#97). Mostly verified by offline unit tests only; QGIS-side behaviour is unverified until the next live smoke test.
    - Fix: execute_pyqgis_script's isolated snapshot no longer renames the live project or clears its dirty flag (F01).
    - Fix: confirming a cloud-data (egress) gate preview now works, survives a re-plan, and no longer injects `confirmed` into tools that do not accept it (F02).
    - Fix: the reply no longer presents tables/results for tool calls that never ran; a "No data was retrieved" warning is appended, and new prompt rules forbid inventing results, hand-converting coordinates and unrequested memory notes (F03, F14, F23).
    - Fix: add_point_layer takes a `crs` and converts projected coordinates in code (an EPSG:3857 origin had landed ~1.3 km off); the local-data offer no longer reads implausible request coordinates as the project CRS (F04, F20).
    - Fix: calculate_service_area ignores a speed_field that is mostly empty, flags a 1-metre "1 hour" cost, and never replaces a good result with a zero-length one (F05).
    - Fix: destructive confirmations require an explicit word (a casual "yes" no longer confirms) and typed confirmations expire after 10 minutes (F16).
    - Fix: the unrequested output "nudge" only fires for an output the user named (F10); expired canvas highlights are removed from the scene (F11); memory notes containing coordinates are refused and an empty Memory dialog explains itself (F23, F24).
    - Change: dashboards default to a timestamped file under data/20_processed/dashboards and show the sampling notice inside the HTML; CSV export writes a UTF-8 BOM and X/Y columns for point layers (F12, F13). Service-area results say their layers are temporary (F06). Sensitivity dialog confirms success (F15).
    - Not fixed in rc8 (open): layer-tree duplicate nodes (F07), 43-minute routing (F08), convex-hull exposure geometry (F09), whole-country WorldPop fetch and silent large downloads (F19, F22), per-turn token display, marker clustering, chat-history export completeness (F25).
    v1.16.0-rc7 (2026-09-28):
    Release candidate 7 for 1.16.0. No breaking changes -- still requires QGIS 4.2+. Everything merged to main since rc6 (PRs #43-#69).
    - Fix: execute_pyqgis_script's isolation worker could hang to the full timeout with no diagnostic on Windows (a wrong interpreter resolving to the QGIS binary itself, ModuleNotFoundError('qgis') from a missing PYTHONPATH entry, a Windows-only select() bug losing stderr, and a too-short handshake timeout for a real desktop cold start) -- all four root-caused and fixed, plus a blank console window during the worker's spawn suppressed.
    - Fix: a local-data download offer could lose track of the original request entirely on a one-letter typo in the reply ("dowmload"), and could name the wrong region -- a neighboring country, or literal (0,0) -- by reading the QGIS canvas's current view centre instead of the coordinate the request itself named.
    - Fix: calculate_service_area's single-band hull polygon had no symbology at all (raw QGIS default), and re-running the same analysis (or a follow-up reusing the same origin) stacked duplicate identically-named layers on top of each other instead of replacing them.
    - Fix: an exported CSV from a Processing-algorithm output layer got an unreadable, unstable auto-generated filename and could not be opened -- exports now always use a clean name under the project's own data/20_processed folder (or the QGIS profile folder for an unsaved project), never an ugly temp-file basename.
    - Fix: the local-data download/online question interrupted every eligible request, even the routine case with no real decision to make, using a typed "reply download or online" prompt. Now decided silently for the routine case; only asks -- via clickable chips, not free text -- when the extract is large/unknown or the connection is poor/offline, naming the reason when that's why it's asking.
    - Security: execute_pyqgis_script now runs in a separate, persistent worker process holding its own QgsApplication, with no Python-level access to this plugin's in-memory credentials or conversation history (IMPLEMENTATION_TRACKER.md §1.11 Phase 1); the existing AST/builtins sandbox still runs inside the worker as defense in depth. The cloud-provider data-protection egress gate's remaining deployment decisions (default mode, override policy, script-execution scope, provider-switch history) are answered and documented.
    - New: a direct "Sensitivity" UI control (dock header button) for tagging a loaded layer's data classification -- previously chat-only.
    - New, opt-in: georeference_image gained transform_type ("tps" default/unchanged, "linear", "helmert"), matching QGIS Georeferencer's own transform vocabulary, and now reports RMSE plus scale/rotation so alignment quality is visible instead of unreported.
    - New: analysis-tool output styling is now remembered per project -- a follow-up request producing a similar output (e.g. the same buffer type again) visually matches the one before it instead of always reverting to one hardcoded default, closing part of the "smart mapping" gap (IMPLEMENTATION_TRACKER.md §1.16; the request-validator/task-planner/tool-router half of that architecture is deliberately not built yet, flagged for its own pass).
    - Fix: uneven map-visualization quality and a routing-speed assumption not accounting for country -- dashboard feature sampling now spreads evenly across the whole dataset, and estimate_road_speeds can be given a country for a more realistic default.
    - Docs/governance: release tags now use the cartogen-ai-v<version> prefix (not commercial-plugin-v<version>, which read as a proprietary-licensing signal on a GPL v2 repo); personal contact info and internal-only business/strategy documents removed from this open-source repo; repo links pointed at this canonical repo instead of a stale sibling.

v1.16.0-rc6 (2026-09-27):
    Release candidate 6 for 1.16.0. No breaking changes -- still requires QGIS 4.2+.
    - Fix: a query naming a time threshold in words ("beyond one hour's travel") no longer gets silently reinterpreted as a 5 km distance default -- the pattern now recognizes spelled-out numbers and plurals ("one hour's", "2 hours", "30 minutes"), not just digits with a singular unit.
    - Fix: ingest_osm_features retries a transient Overpass failure (504/429) instead of spending a whole tool call on it; a road network now ingests as real lines (was: one point per vertex, no lines at all), and a point-typed layer is refused as a road network with a clear error instead of silently producing meaningless results.
    - Fix: an in-place upgrade over a running install no longer fails with a stale-module ImportError -- cached cartogen_ai.* modules are evicted on every load, not just when the plugin folder's path changes.
    - Fix: automatic model selection now actually takes effect (it previously kept sending the configured default on every request) and never escalates to an expensive or special-purpose model.
    - Fix: network analysis (service area, travel-time matrix, delivery routes) now measures in real metres/hours regardless of the layers' CRS or the project's ellipsoid setting, and no longer silently routes from the wrong place when an origin/stop layer's CRS differs from the road network's.
    - Fix: a large road network no longer freezes QGIS during routing -- it now runs in the background with a Stop button and progress, and calculate_service_area routes only over roads that can actually be reached (same result, much faster).
    - Fix: GDACS/NASA EONET alerts now carry their event ID as a layer field, so "buffer around alert X" can select that one alert instead of the whole layer; buffer_analysis gained only_selected to scope an operation to a selection.
    - New, opt-in: estimate_road_speeds writes an assumed per-road-class speed field onto a road network when real speed-limit data is sparse or absent (never run automatically -- the model is told to say the result is an estimate, not the network's real speed limits).
    - New, opt-in: before a road-network request, the assistant now offers to download a full local country/region extract (Geofabrik) instead of relying on the live, rate-limited Overpass API for a small area.
    - Docs: 5 scoping documents evaluating third-party libraries for known pain points (RestrictedPython, Presidio, semantic-router, LanceDB/Chroma, Instructor/Outlines) -- one small, verified fold into the sandbox's denylist shipped; the rest recommend against adoption or further prototyping, not built. The exact-ZIP clean-profile install/upgrade check (docs/IMPLEMENTATION_TRACKER.md #1.10) is now closed with a real QGIS session, not just headless tool code -- toolbar icon, Plugins-menu entry, and an in-place upgrade all confirmed rendering/working cleanly.

v1.16.0-rc5 (2026-09-24):
    Release candidate 5 for 1.16.0. Requires QGIS 4.2 or later: QGIS 3.x support is dropped (qgisMinimumVersion 3.28 -> 4.2), so QGIS 3.x will treat this release as incompatible and users there stay on the version they have.
    - New, all OFF by default: a cloud-provider data-protection gate (Settings > Cloud data protection: Off / Warn only / Block, plus strict) that stops a tool call or a file attachment from sending a layer tagged RESTRICTED/SENSITIVE -- or one derived from it -- to a non-local AI provider; a plan-validation gate requiring create_plan before DELETE/PUBLISH tools; and a Project Inspector that adds print layouts, map themes and project metadata to the model's context.
    - New tool: create_project_folder_structure, an opt-in project folder layout (data/00_raw for immutable source data, staging, processed, exports and more). Only creates missing folders, never overwrites.
    - Behaviour change: run_allowlisted_processing_algorithm now adds an output layer HIDDEN (unchecked in the layer tree) when no output name is given, treating it as an intermediate step. Name the output to keep it visible.
    - Security: the execute_pyqgis_script sandbox now also blocks QgsProject.write() (an unrestricted project write to any path) and QgsApplication.authManager() (auth config ID enumeration). A new always-on prompt rule tells the model never to store personal data in global memory. While the data-protection gate is on, lowering a layer's sensitivity tag needs the user's confirmation.
    - Fix: loading a layer from a URL that doesn't serve geodata (e.g. a GitHub /blob/ page, an API error response, a zip) now names the URL instead of a temp file and says what came back (HTML, zip, XML, JSON that isn't GeoJSON with its top-level keys, an empty file). A layer loaded from a URL is also named after the URL, not the temp copy.
    - Docs: GDPR DPO determination recorded (DPIA required, approved with conditions; DPO signature still pending); scopes for process isolation of execute_pyqgis_script (plus a real benchmark) and for the data-protection gate; README warns to install the release asset, not GitHub's source zip.
    Full automated suite: 2,103 tests passing, 48 skipped. Zero Ruff violations. Only QGIS 4.2.2 is exercised by CI's live tests.

v1.16.0-rc4 (2026-09-23):
    Release candidate 4 for 1.16.0. Codebase security review with live adversarial testing against a real QGIS 4.2.2 session, plus observability follow-ups from the rc3 review cycle:
    - execute_pyqgis_script sandbox: 2 real bypasses found via live adversarial testing and fixed. (1) qgis.utils and processing -- both required, never-blocked modules -- import os/sys at module scope, leaving them reachable as plain attributes (qgis.utils.os.getcwd(), and qgis.utils.sys.modules['subprocess'] handing back the live subprocess/socket/ctypes module objects) with no blocked import statement anywhere in the script. (2) This plugin's own package was never blocked, so a script could read the live in-memory session credential store directly via cartogen_ai.infrastructure.auth. Fixed by blocking the os/sys/modules attribute names (closes the pattern for any allowed module, not just the two found) and adding cartogen_ai to the blocked-module list. Both re-verified live after the fix; 6 new regression tests. Per this file's own established convention, this closes what was found this pass, not a completeness claim -- process isolation, not further denylist patching, is recorded as the intended real fix (docs/IMPLEMENTATION_TRACKER.md §1.11), along with 3 findings from the same pass deliberately left open there (unrestricted QgsProject.write() from inside a script, enumerable authManager() config IDs, and an unverified confirmation-gate-bypass question).
    - Best-effort exception handling: 10 tool-layer `except Exception: pass` sites whose silent failure could hide user-visible degradation (the same failure class as an NameError this cycle already found and fixed) now emit a content-free log_event trace (error class only) while keeping their existing fallback behavior. Genuinely benign guards (logger internals, UI callbacks, cache eviction hooks) were left as-is.
    - CLAUDE.md refreshed: it still described the pre-Phase-11 layout (agent/agent.py, agent/providers), said CI has no QGIS, and omitted the Ruff/packaging checks -- updated to the real core/ + infrastructure/ + processing/ layout and the live-QGIS CI job.
    Full automated suite: 1,987 tests passing, 44 skipped. Zero Ruff violations.

v1.16.0-rc3 (2026-09-21):
    Release candidate 3 for 1.16.0. Completes both remaining stable-release gates left open by rc2's own changelog entry -- Ruff lint, and CI matrix validation -- and root-causes a real intermittent CI crash that a first pass had only waived, through five rounds of independent review:
    - Ruff: 125 violations fixed to 0 (not accepted as documented debt). Every finding individually verified against live usage before removal, not blindly trusted from --fix -- one real false positive from --fix itself was caught and reverted (it removed `requests` imports that 5 provider-client test files depend on for mock patching). A real bug was found and fixed along the way: vector_tools.py's apply_labels() referenced QgsLabelObstacleSettings for polygon obstacle-avoidance labeling without ever importing it, a NameError on every real call to that path, silently swallowed by the tool's own broad exception handling. Live-confirmed the fix against real QGIS 4.2.2.
    - CI segmentation fault: root-caused and fixed, not waived. ~40 test methods across the live-QGIS test suite each create a real QDockWidget via addCleanup(dock.close) -- .close() alone never destroys the underlying C++ object, so live-but-closed widgets (each owning child QTimers) accumulated for the whole run and got garbage-collected unpredictably, sometimes during the interpreter's own shutdown sequence, crashing the process immediately after all tests had already reported OK. Reproduced independently on Windows/QGIS 4.2.2 local AND Linux CI, ruling out the environment-specific theories (a QGIS-4.2.2-only Qt quirk, then a numpy/matplotlib ABI mismatch) an earlier pass had assumed. Fixed with an explicit gc.collect() + Qt event-loop pump before QgsApplication.exitQgis(), so deferred widget destruction runs while QApplication is still alive. Verified 4/4 clean runs locally after the fix versus a reliable crash before it; confirmed in CI with zero Segmentation faults and the fallback waiver never triggering.
    - CI hardening: both qgis-live-tests images (4.2.2, release-3_28) pinned by immutable Docker manifest digest instead of a floating tag, after release-3_28's own moving tag drifted mid-session and briefly reintroduced the crash under a different trigger (a numpy/matplotlib ABI issue already present in that image, unrelated to this repo). A plugin-code-free control process was added alongside the real test run so any future reliance on the fallback waiver is evidence-conditioned, not asserted; the CI teardown script now fails the run if cleanup itself raises, not just if test assertions fail.
    Explicitly still open, not claimed done: the exact-ZIP clean-profile install/upgrade-from-1.15.6 test needs a real interactive QGIS GUI session this project's development environment cannot provide (docs/IMPLEMENTATION_TRACKER.md §1.10) -- the only remaining item before a stable-release decision.
    Full automated suite: 1,981 tests passing, 44 skipped. Zero Ruff violations. Both live-QGIS CI legs confirmed green in an actual GitHub Actions run.

v1.16.0-rc2 (2026-09-20):
    Release candidate 2 for 1.16.0. Security/privacy remediation from an independently re-verified 15-section production-standard audit against v1.16.0-rc1, with two rounds of independent review catching real gaps in the first fix pass:
    - QAction lifecycle leak fixed: plugin_main.py's unload() now disconnects each toolbar/menu action's triggered signal and calls deleteLater() before clearing bookkeeping -- previously only removePluginMenu/removeToolBarIcon were called, leaving actions alive and connected across a load->unload->reload cycle (confirmed via a live QGIS 4.2.2 before/after action-count probe). New tests/test_plugin_main_live.py (4 tests, live-confirmed).
    - Plaintext credential persistence removed entirely (strict policy, not an opt-in consent path): infrastructure/auth.py's save_credential() has no code path left that can write a new API key to plaintext QgsSettings. When QGIS's encrypted QgsAuthManager is unavailable, the key is held in memory for that session only and must be re-entered after restart. Migration code that reads/removes legacy plaintext keys from older versions is retained.
    - Logging moved to structured, metadata-only by default: core/logger.py's new log_event() logs only an allowlisted set of safe fields (tool name, status, duration, correlation ID, provider, error class, counts) -- tool-call and agent-turn logging no longer emit any raw prompt, response, tool-argument, or tool-result content, not even truncated/redacted. An explicit, OFF-by-default log_diagnostic() escape hatch exists for local debugging only, gated behind a setting that is never on by default and warns visibly when used.
    - CI matrix expanded and, critically, actually validated with real passing GitHub Actions runs (not just authored): added a windows-latest leg to the existing test job, and a new qgis-live-tests job running the live-QGIS test suite inside the official qgis/qgis 4.2.2 and release-3_28 docker images (matching metadata.txt's declared 3.28-4.99 range), plus a smoke test that builds the actual release zip and imports from the extracted artifact rather than the dev tree. Fixed 3 real environment-specific CI bugs surfaced by actually running these jobs: a pip self-upgrade failure on the 4.2.2 image, a --break-system-packages flag disagreement between the two pinned images, and an image-specific Qt interpreter-shutdown segfault on 4.2.2 occurring AFTER all tests already passed.
    Two items remain explicitly open, not claimed done: the exact-ZIP clean-profile install/upgrade-from-1.15.6 test still needs a real interactive QGIS GUI session (docs/IMPLEMENTATION_TRACKER.md §1.10), and a pre-existing repo-wide Ruff lint failure (present already at the v1.16.0-rc1 tag commit, unrelated to this candidate) is tracked separately rather than folded into this security-focused release.
    Full automated suite: 1,978 tests passing, 44 skipped.

v1.16.0-rc1 (2026-09-20):
    Release candidate 1 for 1.16.0. Renumbers forward from the already-published v1.15.6 stable release, replacing the v1.5.7-rc1..rc5 line: an independent review confirmed via QGIS's own pyplugin_installer.version_compare.compareVersions() that "1.5.7-rc5" compares as OLDER than "1.15.6" (result: 1.15.6 is newer) -- a real version-ordering defect traced back to a 2026-09-19 renumbering decision, inherited unquestioned through rc1-rc5. 1.16.0 was the version originally intended before that renumbering and was never actually released under any tag, so it is reused here rather than skipping ahead further. compareVersions('1.16.0-rc1', '1.15.6') independently re-verified to return the correct newer-than result before this bump.
    All functional content is identical to v1.5.7-rc5 (see that entry below for full detail: the real Phase 11 architecture restructuring, the AST-sandbox bypass fix, and the 2 keyboard-navigation fixes), plus 2 more real defects the same external review found and this candidate fixes:
    - .github/workflows/tests.yml's release-zip packaging check still asserted the packaged zip contains core/agent/agent.py, renamed to agent_orchestrator.py during the Phase 11 work -- the next CI run would have failed this assertion despite the package itself being correct. Verified the fix locally against the real built zip before committing.
    - README.md carried two stale test-count figures (1,947/1,904) left over from before this cycle's work; the v1.5.7-rc5 changelog entry's own "38-test live suite" claim undercounted itself by 1 (it named both test_agent_live.py and test_chat_widget_live.py but only counted the latter). Corrected everywhere to the real current numbers.
    - Validated all 1,962 automated unit/integration tests passing (40 skipped), and the full 39-test live headless-QGIS Qt suite (tests/test_agent_live.py + tests/test_chat_widget_live.py) passing against a real QGIS 4.2.2 session.

v1.5.7-rc5 (2026-09-20):
    Release candidate 5 for 1.5.7: real Phase 11 architecture restructuring (a prior pass had left it as directory scaffolding only), a live-confirmed AST-sandbox bypass fix, and 2 keyboard-navigation fixes:
    - Phase 11 architecture restructuring finished for real: infrastructure/ (auth.py, deps.py, providers/), core/models/, core/validators/, core/services/ are now genuine physical file moves, not the thin re-export facade a prior pass left behind. agent.py decomposed into 4 files (tool_dispatcher.py, usage_tracker.py, history_manager.py extracted; agent.py itself renamed to agent_orchestrator.py) and chat_tab_widget.py split into 2 subsystems (chat_view_presenter.py, chat_input_controller.py).
    - Security fix: found and closed a novel execute_pyqgis_script AST-sandbox bypass -- type.__dict__['__subclasses__'] reached the same dangerous method the already-blocked classic escape chain uses, via a dict subscript the AST attribute-name check never sees. Live-verified before and after the fix against the real restricted-builtins exec environment.
    - Fixed 2 real keyboard-navigation gaps found via live QTest verification: Tab pressed inside the chat input box only inserted a literal tab character (nothing past the input was reachable by keyboard); Escape did nothing. Both fixed and regression-tested.
    - Validated all 1,962 automated unit/integration tests passing (40 skipped), and the full 38-test live headless-QGIS Qt suite (tests/test_chat_widget_live.py) passing against a real QGIS session.

v1.5.7-rc4 (2026-09-20):
    Release candidate 4 for 1.5.7: ingest_osm_features two-phase OSM vector ingestion, AST sandbox prompt guardrails, and a code-review remediation pass fixing 7 real correctness/security bugs plus 1 latent circular import:
    - Added ingest_osm_features two-phase vector ingestion tool (Overpass API -> GeoJSON -> QgsVectorLayer) enabling autonomous fetching of facilities and infrastructure for empty projects and coordinates.
    - Hardened agent prompt guardrails (Rule 6) around execute_pyqgis_script AST security sandbox to steer to ingest_osm_features when layers are missing.
    - Fixed auth.py credential rotation leaking orphaned QgsAuthMethodConfig entries, and an Ollama settings-key rename with no migration path.
    - Fixed export_tools.py silently exporting a full layer when only_selected=True finds nothing selected, and a regression that reintroduced a blocking modal dialog on the autonomous agent's export path.
    - Fixed humanitarian_tools.py dropping OSM features exactly on the equator/prime meridian (falsy-zero bug), styling_tools.py silently clobbering layer opacity, and a vacuous-truth misclassification bug in the new representation profiler.
    - Fixed a latent circular import between core.agent.auth and infrastructure/__init__.py (CredentialManager re-export made lazy).
    - Correction: RC3's own changelog entry was found to have been edited in place by a later commit to retroactively claim ingest_osm_features/AST guardrails shipped in RC3 -- confirmed via git show against the actual RC3 tag that neither was present. Left RC3's already-tagged entry as-is; both features accurately ship here, in RC4.
    - Validated all 1,958 automated unit/integration tests passing (38 skipped), up from RC3's 1,947 -- 11 new regression tests, one per fix.

v1.5.7-rc3 (2026-09-19):
    Release candidate 3 for 1.5.7: pre-release audit remediation, PyQGIS API modernization, and autonomous vector ingestion:
    - Packaged and synchronized release archives (cartogen_ai.zip and dist/cartogen_ai_v1.5.7-rc3.zip) with all 177 tools, Map Intelligence Engine, and Intelligent Representation Planner.
    - Added ingest_osm_features two-phase vector ingestion tool (Overpass API -> GeoJSON -> QgsVectorLayer) enabling autonomous fetching of facilities and infrastructure for empty projects and coordinates.
    - Hardened agent prompt guardrails around execute_pyqgis_script AST security sandbox to steer to ingest_osm_features when layers are missing.
    - Reconciled minimum supported QGIS version to 3.28 (Firenze LTR) matching Python >=3.9 requirement.
    - Replaced deprecated QgsVectorFileWriter.writeAsVectorFormatV2 with writeAsVectorFormatV3 in export_tools.py.
    - Replaced deprecated QgsGraduatedSymbolRenderer.createRenderer with QgsClassificationMethodRegistry in styling_tools.py.
    - Validated all 1,947 automated unit/integration tests passing (38 skipped) and 5/5 live headless QGIS pipeline tests with zero deprecation warnings.

v1.5.7-rc2 (2026-09-19):
    Release candidate 2 for 1.5.7: documentation synchronization, full-phase engineering self-review,
    and release candidate stabilization:
    - Centralized all settings keys and QgsSettings layer property lookups into infrastructure.settings_keys.
    - Decoupled requests import across all AI provider modules allowing lean testing environments.
    - Added DBF byte-length shapefile preflight laundering with while-loop candidate disambiguation.
    - Added create_shaded_relief combining hypsometric tinting with hillshade via Multiply blend mode.
    - Validated all 1,916 automated tests passing across core, processing, and agent systems.
    - Packaged and verified release archives (dist/cartogen_ai_v1.5.7-rc2.zip and cartogen_ai.zip).

v1.5.7-rc1 (2026-09-19):
    Release candidate 1 for 1.5.7: major architectural and cartographic remediation (Part A & Part B):
    - Added native QGIS Processing Provider (cartogen_ai.processing) with OptimalHubSitingAlgorithm
      and CalculateServiceAreaAlgorithm for Graphical Model Designer, batch processing, and headless
      qgis_process CLI execution. Updated metadata to hasProcessingProvider=yes.
    - Added OGC SLD 1.1.0 style export (export_layer_sld) with fallback for in-memory layers.
    - Added native Point Cluster and Point Displacement renderers (apply_point_cluster_style) wrapping
      vector layers via setEmbeddedRenderer().
    - Established clear architectural subdivisions: cartogen_ai.infrastructure boundary,
      cartogen_ai.core.models, cartogen_ai.core.validators, and cartogen_ai.core.services.
    - Enhanced print layouts with coordinate graticule/grid (auto-interval rounding), MAP_INFO
      CRS and representative fraction scale metadata, and 32x32mm inset overview locator map.
    - Implemented ellipsoidal geodetic distance measurement (QgsDistanceArea) for geographic CRS in
      hub siting, allocation, and delivery route analysis. Fixed 'NONE' ellipsoid string fallback bug.
    - Optimized Gemini API token economy: deterministic alphabetical tool tie-breaking enabling
      Gemini 2.5+ implicit prefix caching (up to 90 percent prompt discount), Rule 5 attribute redundancy
      narrowing, and dynamic max token budget scaling.
    - Added Claude and OpenRouter prompt caching with tail-message cache-control breakpoints.
    - Added custom exception hierarchy (cartogen_ai.core.exceptions) and structured logging
      integrated directly with QGIS Message Log (QgsMessageLog) under "Cartogen AI" panel.
    - Integrated QGIS Network Access Manager proxy settings (get_qgis_proxy_dict) into HTTP sessions.
    - Added create_shaded_relief combining hypsometric tinting with hillshade via Multiply blend mode and opacity validation.
    - Added Shapefile preflight laundering with collision-loop disambiguation and DBF 10-char warnings.
    - 100 percent centralized settings and layer properties into infrastructure.settings_keys with graceful provider import decoupling.
    - Improved cartography defaults: ColorBrewer CVD-safe BrBG ramps, text buffer halos (0.8mm),
      obstacle placement priority, and StdDev/Pretty/Logarithmic classification modes.
    - Added GeoPackage database style persistence, UTF-8 shapefile encoding fallback, and SpatiaLite
      spatial indexing.
    - Enforced thread safety across background task worker threads and GUI thread with RLock guards.
    - Configured ruff and mypy in pyproject.toml, added CI linting and zip packaging validation (<20MB).
    - Full test suite passing: 1,916 tests (0 failures, 38 skipped; +145 tests added).

v1.15.6 (2026-09-18):
    Promoted to stable from v1.15.6-rc6 -- same code, no changes since that candidate. Six
    release candidates on this line (rc1-rc6) were run and checksum-verified against real QGIS
    over 5 days: a 27-point security/QGIS/API/performance/code-quality audit remediation (rc1);
    dependency, GDACS-filter, screen-clamp, and prompt-preview fixes (rc2); a missing-first-
    message-echo fix and tool-argument shape validation (rc3); the "'str' object has no
    attribute 'get'" crash finally root-caused for real after 5+ prior same-text incidents, a
    reverted UI-freezing custom layout, and a Settings/chat/Activity visual pass (rc4); a task-
    router confidence-floor fix, a field-width data-loss fix, a destructive-action confirmation-
    gate safety fix, and the full 4-phase Broadsheet redesign -- single-scroll dock, inline
    safety-gate card, layer context picker (rc5); a dead search-dependency migration found by
    independently verifying every remaining open item from two rounds of external audit rather
    than leaving them unverified (rc6). Full detail for every fix: CHANGELOG.md.
    Known, honestly-tracked open items at this promotion, not resolved by it: PostGIS live
    read-only-SQL enforcement has never been verified against a real database in this
    development environment; a live Gemini network observation from this same environment is
    blocked on an interactive credential-store unlock; imagery feature extraction
    (ultralytics/torch) is confirmed installable but not end-to-end tested against a real QGIS
    raster layer; and the Community/Professional/Enterprise licensing path
    (docs/PRODUCT_TIERS.md) remains an open business/legal decision that does not affect this
    edition's shipped functionality. See docs/IMPLEMENTATION_TRACKER.md for the current,
    living list.
    Full suite: 1754 tests, 0 failures.

v1.15.6-rc6 (2026-09-18):
    Release candidate: one real fix, found while closing out this project's own two-round
    external audit of RC5 by actually verifying every remaining open item rather than leaving
    them as "reported PASS, not independently rerun." search_web's dependency, duckduckgo-search,
    turned out to be genuinely broken -- it silently returns zero results for a real query, even
    at its own pinned floor version, with no exception raised for the tool's own error handling
    to catch, so a web search just quietly reported "no results found" as if that were a normal
    answer. Confirmed live: the same query against ddgs (the actual current package the
    dependency was renamed to upstream) returned real results immediately. search_web now imports
    ddgs first, falling back to duckduckgo_search only if ddgs genuinely isn't installed -- both
    expose the same DDGS class/.text() call shape, so no logic below the import needed to change.
    requirements.txt and the startup dependency-check banner now track ddgs instead of the dead
    pin. The audit pass itself also resolved two false alarms without any code change needed (a
    release-checksum non-issue caused by this build script's zip entries embedding real file
    mtimes, and a "provider licensing contradiction" traced to a clearly-marked, never-merged
    draft proposal being misread as current policy) plus two small real doc corrections (a stale
    tool count, a leftover "commercial" phrase inconsistent with the rest of the README) -- see
    CHANGELOG.md for the full trail. Also independently confirmed: this machine has only one real,
    complete QGIS install (4.2.2) despite 3 other version directories present, and PostGIS/live-
    Gemini verification remain genuinely blocked on things only a human can unblock (a one-time
    interactive Docker Desktop setup; either a live query run by hand or explicit authorization to
    drive the real QGIS GUI and spend real API quota) -- not silently declared done. Full suite:
    1754 tests, 0 failures.
    Still a release candidate, not final.

v1.15.6-rc5 (2026-09-17):
    Release candidate: 3 real fixes from an extended live-testing session on top of rc4, plus a
    full Broadsheet visual/structural redesign of the dock UI. Fixes, all reproduced against the
    real agent pipeline: (1) the task router's own confidence floor was computed but never
    checked -- a low-confidence match (e.g. "apply a color ramp to the raster layer" scoring
    0.33) still got baked into a "recognised task" system-prompt directive, routing the model to
    a completely unrelated deliverable; now a below-floor match falls through exactly like a
    genuinely unmatched query. (2) External API string values wider than a memory layer's
    shapefile-era field width (e.g. GDACS's multi-country `country` field) were silently dropped
    by QGIS's own memory-provider width enforcement, with nothing surfacing it -- now clipped
    defensively at every write site. (3) The most serious: a destructive-action confirmation
    gate could be silently bypassed by normal chat use -- a user replying "Confirm" in the chat
    box (not the Activity tab's own button) re-entered the free-form LLM loop with no structured
    awareness of what was pending, and the model fabricated a "Confirmed" narrative without ever
    re-calling the tool, so an approved edit silently never happened. Fixed at 3 compounding
    layers: a pending confirmation now always lands on a dedicated task instead of overwriting an
    unrelated one, a typed "Confirm"/"cancel" resolves through the exact same deterministic path
    the Activity tab's button always used, and the pending tool/arguments are now surfaced in the
    model's own prompt context as a defense-in-depth backstop.
    Redesign ("Broadsheet", from a user-supplied mockup board, confirmed direction by direction):
    the Chat/Activity tab split is gone in favor of one continuous scroll with a sticky plan strip
    that states e.g. "3/4 steps complete -- 1 needs you" directly; the plain-text destructive-
    action gate is now a real inline card with clickable Apply-edit/Cancel links wired to the same
    safe confirmation path above; the Task Inspector and Project Notes/Memory sections moved from
    always-docked tab content into their own dialogs; and a new layer-context picker gives
    per-question, opt-out control over which loaded layers' schema reaches the model, defaulting
    unchecked for any layer already tagged sensitive via the existing sensitivity tools. New
    docs/LIVE_TEST_SCENARIOS.md adds 5 multi-turn workflow scenarios verified against real canvas/
    attribute-table state, not chat prose. USER_GUIDE.md and the in-app Help window updated to
    match. Full suite: 1751 tests, 0 failures; 34/34 live-QGIS tests passing.
    Still a release candidate, not final.

v1.15.6-rc4 (2026-09-16):
    Release candidate: a large batch of live-reported fixes on top of rc3, every one
    reproduced and verified against the packaged plugin, not just the dev tree. Two real
    crashes, root-caused with actual tracebacks (not guessed): (1) a tool's pre-execution
    "snapshot" step (apply_categorized_style and similar MODIFY/DELETE tools) received the
    model's tool-call arguments as a raw, still-JSON-encoded string instead of a parsed
    dict, crashing with "'str' object has no attribute 'get'" on every turn that called one
    -- this is the exact crash reported repeatedly this cycle at every different tool-call
    count (18, 3, 1, 15), now conclusively fixed and covered by a regression test
    reproducing the real traceback; (2) a defensive guard against the same error class in
    the mid-turn tool-result compaction path. A related token-budget bug: a canvas-
    screenshot tool call could blow a provider's token ceiling on its own because oversized
    results only got trimmed after 8 other tool calls -- now trimmed immediately after their
    one necessary use. A real tool-discovery gap: geocode_and_enrich/geocode_batch shared no
    vocabulary with "health facilities beyond one hour's travel"-style requests, so they lost
    the competition for a slot in the ~40 tools shown to the model on every such request --
    the model resorted to hunting the filesystem via execute_pyqgis_script instead of ever
    seeing the geocoding tools existed; fixed with router aliases, confirmed end-to-end (the
    real agent.run() loop now actually offers both tools to the model for this exact
    request), and covered by a new live regression test that scripts a full realistic turn
    (geocode -> create a plan -> add a real QGIS layer -> style it -> mark every task DONE)
    and confirms a real layer appears on the canvas and the Activity tab's data actually
    populates. A UI freeze: a custom Qt layout added for the Settings dialog's provider pills
    (then reused in the Activity tab) triggered a real, live-reported total application
    freeze on its first real interactive use -- reverted outright to plain, long-established
    Qt layouts rather than risk the same failure mode again. The Settings dialog and chat
    panel also got a substantial visual pass (numbered sections, a masthead status line, a
    "Keys" summary with masked values, a redesigned welcome message and prompt-preview card,
    an Activity-tab button-overflow fix, and a fix for the Activity tab's memory panel
    showing raw unrendered markdown) following a design proposal drawn up and iterated
    against real screenshots this cycle. Full suite: 1695 tests, 0 failures, including a new
    live-QGIS regression suite (tests/test_agent_live.py) that exercises real tool execution
    against a real QGIS project, not mocks. See CHANGELOG.md for full per-fix detail.
    Still a release candidate, not final.

v1.15.6-rc3 (2026-09-15):
    Release candidate: 2 more fixes on top of rc2, both from direct live reports, both verified
    against the packaged plugin. (1) The message that triggers an in-chat gate question
    (requirement gate or prompt preview) is now echoed as its own chat bubble immediately --
    previously it only appeared secondhand, quoted inside the AI's own response, which looked
    to the user like their message had vanished. (2) Tool-call arguments are now validated to
    actually be an object after JSON-decoding, not just successfully parseable -- a
    double-JSON-encoded arguments string (a known real-world LLM tool-calling quirk) previously
    decoded to a plain string and crashed uncaught on the next line; investigated as a candidate
    cause for a live-reported crash after a real multi-tool-call Gemini turn (not confirmed as
    the exact site -- no traceback was available -- but a real, demonstrable gap in the same bug
    class as two earlier-fixed usage-parsing crashes). Full suite 1689 tests, 0 failures.
    Live-verified: full 16-category checklist (15/15) plus 2 targeted checks for both fixes
    against the rebuilt package. See CHANGELOG.md for full detail.
    Still a release candidate, not final.

v1.15.6-rc2 (2026-09-15):
    Release candidate: 4 real fixes on top of v1.15.6-rc1, all reported live and verified
    against the packaged plugin, not just the dev tree. (1) Declared `requests` as a required
    dependency -- every provider client imports it directly, but it was never listed, working
    only because QGIS's bundled Python typically ships it already. (2) Added a `country`
    parameter to fetch_gdacs_disaster_alerts -- a place-scoped request with no bbox (e.g.
    "GDACS alerts for Yemen") previously fell through to every alert on Earth; now scopes by
    GDACS's own per-alert country field. (3) The floating chat dock now proactively re-clamps
    to the available screen when the prompt-preview/refinement panels show or hide, closing a
    timing gap in the existing screen-clamp guard that could leave the input row under the
    Windows taskbar. (4) Converted the "Prompt that will be sent" boxed panel into an in-chat
    exchange (reply to confirm, type a correction, or say "cancel") -- the same conversion
    already applied to the requirement-gate panel, now covering the last boxed panel in the
    chat flow. Full suite 1684 tests, 0 failures. Live-verified: full 16-category interactive
    checklist (15/15 runnable categories) plus 4 targeted checks (plugin load/unload via the
    real classFactory() entry point, a live network call proving the Yemen filter actually
    scopes results, the screen-clamp firing on a real panel change, and the old boxed panel
    confirmed removed) -- all against the packaged zip. See CHANGELOG.md for full detail.
    Still a release candidate, not final: licensing/publication decision, package provenance
    confirmation, and stable-promotion gates remain open.

v1.15.6-rc1 (2026-09-14):
    Release candidate: audit remediation, 27 of 32 findings fixed/mitigated from a full
    security/QGIS/API/performance/code-quality audit (docs/audits/). SSRF-hardened 4 URL
    fetches, sanitized CSV formula injection, redacted provider error bodies that could leak
    partial API keys, fixed a crash on malformed provider JSON, excluded hazard-fetch tools
    from recurring schedules (closing a real GUI-freeze risk), added real tests for all 24
    previously-untested tools, and more -- see CHANGELOG.md for the full breakdown. Test
    suite 1531 -> 1680, 0 failures. Tagged as a release candidate, not final: an interactive
    QGIS smoke test, a licensing/publication decision, and package provenance confirmation
    remain open. Also added CI secret scanning (gitleaks) and enabled Dependabot alerts.

v1.15.5 (2026-09-13):
    Fix: another instance of the "'str' object has no attribute 'get'" crash, reported 3 more
    times after v1.15.4 on a Gemini-configured session with different tool sequences each
    time. Root cause: extract_openai_style_usage's cached-tokens extraction (added in v1.15.0)
    used `(x or {}).get(...)`, which only degrades safely when x is falsy -- a truthy non-dict
    response field (a bare string, the likely real shape) crashes it uncaught. Fixed with an
    explicit isinstance check. This is new code from the Gemini-caching work, not covered by
    the v1.15.4 tool-dispatch audit. See CHANGELOG.md for full detail.

v1.15.4 (2026-09-13):
    Fix: root-caused the "Error: 'str' object has no attribute 'get'" crash reported live.
    The two-phase tool dispatch path (used by fetch_worldpop_population and the 3 hazard-
    monitoring fetch tools, among others) had no exception handling of its own, unlike the
    regular tool dispatch path -- and 3 of those network-fetch functions called .get() on a
    freshly-parsed API response without checking it was actually a dict first. If the
    external API ever returned valid JSON that wasn't a dict (a bare string/list/null error
    body), the crash escaped the whole tool-calling loop uncaught. Fixed with a general
    safety net (any exception from any tool dispatch path now becomes a normal error result,
    closing this class of bug for every tool, not just the ones found) plus proper guards at
    the 3 specific sites for a clear error message. See CHANGELOG.md for full detail.

v1.15.3 (2026-09-13):
    Fix: following v1.15.2's move of the requirement-gate question into the chat, a
    real-session report found "some of my text i sent in the chat is not showing" -- the
    user's reply to a clarifying question was folded into the pending request silently,
    never shown as its own chat message. For a request needing two unresolvable details in
    a row, asking a second time replaced the first question with no trace the first answer
    was ever seen. Fixed: the reply is now echoed into the chat log immediately, before
    being merged. Live-verified: a 2-round clarification now shows both replies in order.
    See CHANGELOG.md for full detail.

v1.15.2 (2026-09-13):
    Fix: the task-register requirement gate ("one more detail needed" -- asked when a slot like
    hazard type has no safe default to guess) showed as a separate boxed panel with two buttons
    and a locked input box, disconnected from the conversation above it. Direct user feedback
    with a screenshot: "make it more in the chat and get user response interactive chat." The
    question now posts as a normal Cartogen chat message, and the input box stays fully live --
    answering is just typing a reply and hitting Send like any other turn. Also removed a dead
    "Proceed with stated defaults" button (it was always disabled on every real call). Live-
    verified in real QGIS. See CHANGELOG.md for full detail.

v1.15.1 (2026-09-13):
    Fix: live-hazard-data requests ("show live incidents/disasters/hazards in X") could get a
    blanket "no live data access" refusal even though this plugin's Live Hazard Monitoring tools
    (fetch_gdacs_disaster_alerts, fetch_nasa_eonet_events, fetch_nasa_active_fires, v1.9.0) exist
    for exactly this. Found via a live user report with real typos ("haszard", "incedent"), root-
    caused via direct router simulation: no prompt rule connected "live hazard/disaster" requests
    to these tools, and even when a tool WAS a router candidate, generic words like "map"/"layer"
    made its inclusion in the top-40 a coin flip against ~24 other tools scoring similarly. Fixed
    with a new prompt rule, new tool-router aliases for the 3 hazard tools, and a small typo-
    tolerant correction so a misspelled "hazard"/"incident" still resolves. A mixed request (e.g.
    hazards + crime) now fetches what real tools cover and honestly states what has no source,
    rather than refusing the whole thing. See CHANGELOG.md for full detail.

v1.15.0 (2026-09-13):
    Gemini prompt caching, direct request ("implement gemini caching, check the
    documentation"), following a real report of ~168K tokens for one 6-call print-layout turn.
    Researched Google's current Gemini API docs before writing any code: implicit caching is
    automatic and free for Gemini 2.5+/3.x models -- no client code needed to trigger it, ~90 percent
    discount on cache hits -- as long as the cached content is a stable prefix above a per-model
    minimum (2,048-4,096 tokens). Confirmed this plugin's system prompt/tools already meet both
    requirements (computed once before agent.py's tool-calling loop, never rebuilt mid-turn), so
    a multi-call turn's repeated system prompt is exactly the shape implicit caching targets --
    nothing to implement there beyond confirming it isn't accidentally defeated. What WAS
    missing was visibility: the Gemini OpenAI-compatible endpoint reports cache hits via
    usage.prompt_tokens_details.cached_tokens, but nothing surfaced it. Explicit caching (a
    separate CachedContent resource with its own lifecycle/storage cost) was considered and
    deliberately not built -- implicit caching already covers the reported scenario for free,
    and the per-turn module-based system prompt (v1.14.0) limits explicit caching's cross-turn
    reuse value for the added complexity. Extended usage extraction (shared by
    Gemini/OpenAI/OpenRouter/Ollama) to surface cached_tokens; extended Claude's own usage
    extraction the same way for symmetry (Claude already sends cache_control breakpoints from
    earlier this session, but never surfaced cache_read/cache_creation counts either) --
    Anthropic's own "input_tokens" excludes cached tokens unlike Gemini/OpenAI's convention, so
    it's reconstructed to the full request size for cross-provider consistency. Session usage
    now shows e.g. "~24,747 tokens this session (3 calls, ~14,400 served from cache)". No new
    agent tools; 169 tools unchanged. 18 new tests, full suite 1506 -> 1512, 0 failures.
    Live-verified end to end in real QGIS 4.2.2. Honest limitation: no live network access to a
    real Gemini API key in this sandbox, so the parsing logic is confirmed against documentation
    and realistic mock responses, not a real live API response. See CHANGELOG.md's [1.15.0]
    entry for full detail.

v1.14.1 (2026-09-13):
    Patch release, direct live report: after a long print-layout response, the chat input row
    was no longer visible -- the floating panel appeared to end right where the response
    content did. Investigated thoroughly before changing anything (small window + long
    content, a bloated Activity tab, the exact reported 6-call/multi-tool-step scenario) --
    could not reproduce a layout defect in headless testing; chat_tab_widget's layout
    correctly kept the input row visible in every scenario tried. A floating window that's
    grown or drifted past the available screen's height/position is a known Qt/Windows
    failure mode that produces exactly this symptom -- added a defensive resizeEvent guard
    that clamps a floating dock's geometry to QScreen.availableGeometry() whenever it would
    otherwise extend past it (idempotent, only acts while floating). Live-verified: forced the
    dock to an oversized/off-screen geometry and confirmed the guard restores it on-screen with
    the input row reachable. No new agent tools; 169 tools unchanged. Full suite unaffected:
    1506 tests, 0 failures. See CHANGELOG.md's [1.14.1] entry for full detail.

v1.14.0 (2026-09-13):
    Prompt modularization release, follow-up to v1.13.1's tool-router token fix. The remaining
    ~8,149-token base system prompt was investigated (not assumed): it contains no JSON schemas
    at all, 100 percent plain-English prose -- 47 numbered behavioral rules always sent in full
    regardless of what the request needed. Categorized all 47 rules into three tiers: 16 CORE
    rules (task/memory mechanics, the destructive-action gate, anti-fabrication, output
    structure) always included; a 10-rule SENSITIVE cluster (protection-sensitive data handling,
    never-fabricate-security-incidents -- rule 42 exists because of a real, cited past incident
    -- operational-briefing standards, sensitivity-check-before-export) included whenever any
    tool from a deliberately wide, hand-verified trigger list is selected (every export tool,
    incident/sensitivity tool, humanitarian-logistics tool -- not narrowly gated to "sounds
    humanitarian"), confirmed with the user via AskUserQuestion before implementing; the
    remaining 21 domain rules (styling, imagery, workflows, documents, forecasting, web search,
    print layouts) auto-triggered by tool names already present in each rule's own text, via a
    one-time regex scan against the real tool registry. Found and fixed a real bug during live
    verification: router-guaranteed tools (execute_pyqgis_script, the 8 always-include core
    tools) were polluting the auto-extraction, causing print-layout rules to fire on a plain
    "hi" -- fixed by excluding router-guaranteed tools from the trigger signal. Every rule keeps
    its original number permanently (20 cross-references by number exist across the 47 rules),
    and the full/unfiltered prompt is verified byte-identical to the pre-modularization content.
    No new agent tools; 169 tools unchanged. Measured effect: a real agent.run("hi") call's full
    payload dropped from ~9,316 tokens (v1.13.1's number) to ~3,184 tokens -- ~84 percent below the
    original ~22,959-token report that started this whole thread. Real GIS requests save
    15-30 percent depending on domain. Live-verified against real QGIS 4.2.2 for a plain "hi", an
    export request, a humanitarian-logistics request, and a print-layout request, plus a spot
    check across 7 realistic humanitarian-flavored queries confirming the sensitive cluster
    fires on every one. Full suite: 1506 tests, 0 failures. See CHANGELOG.md's [1.14.0] entry
    for full detail.

v1.13.1 (2026-09-12):
    Patch release, found and fixed the same day v1.13.0 shipped: a direct live report that a
    single plain "hi" message cost ~22,959 tokens for one API call. Traced to
    tool_router.py's filter_relevant_tools() always padding out to the full top_k=40 tool
    schemas even for a query with zero real signal, worsened by two real matching bugs -- raw
    substring containment (not word-boundary matching) let "hi" spuriously match inside
    unrelated tool names (histogram_equalization, highlight_features, hillshade), and even a
    correctly-detected no-signal query still padded with random 0-score filler instead of
    returning just the always-useful 9-tool core set. A follow-up stopword list (articles,
    pronouns, common greetings) closes a related gap where a real English word like "there"
    (genuinely present in several tool descriptions' ordinary prose) still pulled in the full
    padded set. Every real-signal query (buffer, calculate severity index, NDVI, all existing
    alias-coverage tests) is completely unaffected -- still gets the full top_k=40 candidate
    pool exactly as before. Measured effect: "hi"'s full agent.run() payload dropped from
    ~22,959 to ~9,316 tokens in live verification against a real QGIS 4.2.2 session. No new
    agent tools; 169 tools unchanged. Full suite: 1488 tests, 0 failures. See CHANGELOG.md's
    [1.13.1] entry for full detail.

v1.13.0 (2026-09-12):
    Rate-limit/task-size resilience release. A genuinely complex multi-step request could burst
    up to 20 back-to-back API calls with zero pacing, and each call re-sent every prior tool
    result in full -- real risk of tripping a provider's rate limit or a context-length ceiling,
    with no automatic mitigation before this (only a canned message once the turn had already
    failed). Investigated actual behavior before changing anything: OpenRouter's client already
    had real rate-limit resilience (model-fallback chain + a 30s-wait/3-cycle retry loop);
    Claude/OpenAI/Gemini/Ollama only had a short 1.5s/3s generic backoff, nowhere near enough to
    outlast a real per-minute quota window. Three fixes: (1) providers/base.py's post_with_retry
    gets a 429-specific retry budget (10s/20s/30s, roughly a minute of patience) separate from
    the generic 5xx one, lifting all 5 providers to a similar level of sustained-rate-limit
    resilience; also fixed gemini.py's grounded_search(), the one raw requests.post() call found
    bypassing the shared retry helper. (2) agent.py: adaptive inter-iteration pacing -- a normal,
    small turn (<=3 tool-call rounds) pays nothing at all; only a genuinely large turn starts
    spacing its own remaining calls out, proportional to task size. (3) agent.py: mid-turn
    context compaction -- once a turn accumulates more than 8 tool results, older SUCCESSFUL
    ones get replaced with a placeholder (any result containing an error is never compacted, at
    any age -- failures stay load-bearing, matching v1.12.0's chat-tool-steps redesign's
    identical call). No new agent tools; 169 tools unchanged. Live-verified against a real
    CartogenAi agent instance and real tool dispatch (get_layers against a real QgsProject) in
    real QGIS 4.2.2: a 20-iteration looping turn produced exactly 17 pacing sleeps and correctly
    compacted 11 old results while keeping the most recent 8 in full. Full suite: 1484 tests (up
    from 1478), 0 failures. See CHANGELOG.md's [1.13.0] entry for full detail.

v1.12.0 (2026-09-12):
    Third round of the same real-session feedback thread. Fixed a real regression in v1.11.0's
    own chat-bubble accent stripe: it looked correct in code and in a screenshot, but the
    border-left CSS was silently dropped by Qt's rich-text table engine when a border shorthand
    was already set on the same cell -- confirmed by rendering an unmistakable magenta
    border-left in isolation and finding it never appeared at all. Fixed with a two-cell table
    (a dedicated stripe cell using only background-color, next to a content cell using only a
    3-sided border) -- verified this time by isolating just the QTextBrowser and checking for a
    narrow, spatially-consistent run of the exact color, not merely that the color existed
    somewhere in a wider screenshot. Also removed the disabled version menu entry added in
    v1.11.0 (one too many places showing it, per direct same-day follow-up) -- version now shows
    only inside the Help dialog.
    New: Help now auto-shows the first time the plugin ever loads, and again once after any
    version update. New first-use onboarding profile (agent/onboarding_profile.py,
    ui/onboarding_dialog.py) -- a short dialog asking role/use-case, QGIS experience level, and
    communication style, saved as a real, human-readable user_profile.md in the QGIS profile
    directory (not a hidden settings blob), feeding the base system prompt on every request via
    a new build_system_prompt(user_profile_ctx=...) parameter; re-editable via Settings -> "Edit
    My Profile...". Deliberately a static dialog, not an LLM-driven conversation, since
    onboarding has to work before any API key is configured. Chat tool-call display redesigned:
    every tool call used to render 2 separate scrollback lines (too much visual space/raw
    detail/noise per real feedback); "running" status now updates the existing status label in
    place, and terminal steps are collected into ONE compact "N tool calls · name, name" summary
    per turn with a click-to-expand "Details" toggle (this chat log's first internal clickable
    anchor) -- any failed step's error text always shows regardless of toggle state, never
    collapsed. No new agent tools; 169 tools unchanged. Live-verified against real QGIS 4.2.2
    throughout, including a 2-turn toggle scenario proving the cursor-position shift-adjustment
    behind the Details toggle is actually correct, not just plausible-looking. Full suite: 1478
    tests (up from 1457), 0 failures. See CHANGELOG.md's [1.12.0] entry for full detail.

v1.11.0 (2026-09-12):
    Real-session UI fixes release, from a direct live user report (screenshots from the user's
    own QGIS session, not this project's own synthetic verification renders). Fixed two genuine
    bugs the synthetic screenshots never caught: chat bubbles had two stacked rendering defects
    (a "double border" from a missing border="0" on the outer alignment table, and a
    border-radius:8px that had NEVER actually worked -- Qt's QTextBrowser rich-text engine has
    no border-radius support at all, unlike the QSS styling everything else in this dock uses) --
    replaced with a real border-left accent stripe Qt's engine genuinely renders; and a Qt
    plain-text mnemonic-parsing bug ("Tasks & Notes" rendering as "Tasks _Notes") fixed by
    renaming the tab to "Activity" and spelling out "and" in 3 more affected widget labels found
    by sweeping for the same pattern. Also: Help moved out of the dock's own tab bar entirely
    into the QGIS Plugins menu as a standalone dialog, with a new disabled "Cartogen AI vX.Y.Z"
    menu entry (and the same version line atop the Help dialog) so version info is discoverable
    from the main menu; the quick-suggestion chip row removed from the bottom of the Chat tab
    (still backs Help's own example-prompts list); and the Activity tab's QScrollArea now forces
    ScrollBarAlwaysOff horizontally so content wraps instead of showing both scrollbars at once.
    No new agent tools; 169 tools unchanged. Live-verified against real QGIS 4.2.2: 2 tabs (down
    from 3), no chip buttons, horizontal scrollbar policy confirmed off, HelpTabWidget builds
    standalone with a version string, regenerated dock-panel.png/chat-conversation.png. Full
    suite: 1457 tests, 0 failures. See CHANGELOG.md's [1.11.0] entry for full detail.

v1.10.0 (2026-09-12):
    UI & Chat Redesign release: a visual polish pass on the dock/chat UI, grounded in real
    QGIS plugin design research (not generic web-design advice). New ui/theme.py two-tier
    theme detection (QFieldSync's real pattern: check QGIS's named theme before falling back
    to palette luminance). New _brand_accent() in chat_formatting.py blends QGIS's own live
    accent color toward the Cartogen brand teal -- the first brand color to reach the running
    UI at all, still adapting per QGIS theme rather than becoming one fixed color. New
    ui/icons.py: hand-authored, theme-reactive SVG icons (attach/send/stop/settings) replacing
    plain Unicode emoji, colored from the live palette at construction time. Toolbar/menu icon
    switched from a flat PNG to the real brand mark (a real packaging trap -- pointing directly
    at the excluded branding/ folder -- caught and fixed before shipping). One gate-panel
    button wording aligned for consistency. No new agent tools; 169 tools unchanged. Live-
    verified against real QGIS 4.2.2 via actual rendered screenshots of the wired-up UI in
    both light and dark themes, not just import-success checks. See CHANGELOG.md's [1.10.0]
    entry for full detail.

v1.9.0 (2026-09-12):
    Live Hazard Monitoring release. New hazard_monitoring_tools.py: fetch_nasa_active_fires
    (NASA FIRMS VIIRS active-fire detections, needs a free API key added to Settings),
    fetch_nasa_eonet_events (NASA EONET natural event tracker, free/no key),
    fetch_gdacs_disaster_alerts (GDACS disaster alerts, free/no key), and a composite
    generate_situation_dashboard tying all three into one dashboard export call. Inspired by
    a competitor product's live-event dashboards, scoped Community-tier and kept narrowly
    humanitarian-GIS-focused per a dedicated strategy-alignment research pass against this
    project's own commercial strategy docs -- no cross-domain "intelligence" scoring, no
    country-instability-index concept, no headless/always-on monitoring imported.
    All 3 fetch tools wired into the existing run_monitoring_workflow/schedule_recurring_workflow
    scheduler for real diff-since-last-run tracking ("N new fire detections since last check"),
    and replace their own layer's features in place on repeat calls rather than accumulating
    duplicates, so they're safe to schedule. generate_html_dashboard/generate_temporal_dashboard
    gained per-layer freshness badges (green "Fresh 2m" / amber "Stale 3h" / red "Very stale 2d")
    for any layer stamped with a live fetch time -- purely additive, existing callers unaffected.
    Bbox convention for all 3 tools is deliberately [min_lon, min_lat, max_lon, max_lat],
    matching search_stac_satellite_imagery, not fetch_building_footprints's confusing
    [south, west, north, east]. Live-verified against real QGIS 4.2.2 and the real NASA/GDACS
    APIs throughout, including the monitoring-scheduler diff cycle and the Settings dialog's new
    FIRMS key field. See CHANGELOG.md's [1.9.0] entry for full detail.

v1.8.3 (2026-09-12):
    Patch release: fixed BUG-2026-09-12-2, found and fixed the same day v1.8.2 shipped, caught
    while verifying the v1.8.2 release zip installs cleanly. plugin_upload.py's EXCLUDE_FILES
    matches by basename regardless of directory -- 4 of the 5 docs moved into docs/archive/ in
    v1.8.2's repo-organization pass (IMPLEMENTATION_TASK_LIST.md, LICENSE_AUDIT.md,
    CARTOGEN_AI_PRD.md, CARTOGEN_AI_FEATURE_LIST.md) were still excluded from the release zip as
    a leftover from when they lived at the repo root, so they silently didn't ship. Did not
    affect plugin functionality -- confirmed via a real QGIS 4.2.2 headless import of the
    extracted zip that it installs and runs correctly regardless. Fixed by removing those 4
    entries from EXCLUDE_FILES; rebuilt and re-verified all 5 moved docs now ship. See
    CHANGELOG.md's [1.8.3] entry and docs/BUG_TRACKER.md's BUG-2026-09-12-2 for full detail.

v1.8.2 (2026-09-12):
    Documentation-only release: no code, tool, or behavior changes. Moved 5 frozen historical
    docs (CARTOGEN_AI_FEATURE_LIST.md, CARTOGEN_AI_PRD.md, DOCUMENTATION.md,
    IMPLEMENTATION_TASK_LIST.md, LICENSE_AUDIT.md) from the repo root into docs/archive/ (git mv,
    content unchanged, history preserved); fixed stale "131 tools" mentions to the current 165;
    added a table of contents and a regrouped/expanded Documentation table to README.md
    (surfacing the GDPR/DPIA compliance docs that had no discoverable path from the README
    before); added a docs/README.md index; added a table of contents to SECURITY.md; and gave
    CHANGELOG.md's 4 previously-undated recent version headers real dates plus a "recent
    releases at a glance" quick-index. See CHANGELOG.md's [1.8.2] entry for full detail.

v1.8.1 (2026-09-12):
    Patch release, direct live user report: an exported dashboard's basemap showed a
    blocked/unavailable OpenStreetMap tile message, and the QGIS canvas doesn't follow layers
    the agent just created/modified after a request finishes. Fixed: generate_html_dashboard/
    generate_temporal_dashboard switched from raw OpenStreetMap tiles (blocked by OSM's own
    bulk-use tile policy) to CartoDB Positron; a new zoom_to_layers() now moves the canvas to
    the combined extent of whatever layer(s) a turn's response mentions, once per turn. A real
    bug (single-point layers being silently skipped) was caught and fixed live before shipping.
    Live-verified against real QGIS 4.2.2. See CHANGELOG.md's [1.8.1] entry and
    docs/BUG_TRACKER.md's BUG-2026-09-12-1 for full detail.

v1.8.0 (2026-09-12):
    Cartographic Intelligence release: adapts an external "QGIS Cartographic Intelligence
    Standard for AI Agents" document to this plugin's own architecture, as an integration/
    gap-closure pass on real existing infrastructure. New recommend_visualization_method
    (advisory tool -- inspects real geometry/field/distribution and recommends a styling
    tool, never applies one itself) and apply_rule_based_style (a real QgsRuleBasedRenderer,
    for fixed-vocabulary fields like route status, with a mandatory "Unknown / No data"
    catch-all class). generate_map_product_qa_checklist now reads real layer sensitivity
    tags instead of a static stub, plus two new informational checks (classification
    sanity, export integrity). dataset_status.py's previously-unused CARTOGRAPHY_READY ->
    PUBLICATION_READY transition now has a real, opt-in, overridable blocking QA gate.
    calculate_service_area's travel_cost accepts a list for a real isochrone/access-band
    map, auto-styled in one call. Four new behavioral prompt rules (missing-data-not-zero,
    a conversational MapBrief before finished deliverables, choropleth denominator
    honesty, sensitivity surfaced before export). Live-verified against real QGIS 4.2.2
    throughout. See CHANGELOG.md's [1.8.0] entry for full detail.

v1.7.1 (2026-09-11):
    Patch release: fixed BUG-2026-09-11-1, found and live-verified the same day v1.7.0
    shipped. create_print_layout's portrait orientation had a body-text box only sized for
    ~1 line, and QgsLayoutItemLabel doesn't clip -- longer body_text silently overflowed into
    the safety-critical disclaimer footer. The footer is now pinned to a fixed page-bottom
    position, and a new _fit_text_to_box() truncates body_text to what the box can actually
    hold before rendering. Live-verified against real QGIS 4.2.2 with the exact repro that
    found the bug. See CHANGELOG.md's [1.7.1] entry and docs/BUG_TRACKER.md for full detail.

v1.7.0 (2026-09-11):
    Security & Logistics release, scoped for a humanitarian org (UN/NGO) deployment/pilot.
    GDPR F6-F9 closure: persistent project-memory writes (sidecar file + embedded project
    property) now opt-in/default-off, matching chat history's existing pattern; new "My Data"
    export assembles project memory + global memory + chat history into one JSON document.
    Undo/rollback now covers a priority subset of MODIFY/DELETE tools (remove_layer,
    field_calculator/calculate_area/calculate_length, style changes, dataset-status/
    sensitivity/confidence/run_query) via a new snapshot registry. New sandbox Tier 2:
    run_allowlisted_processing_algorithm runs one Processing algorithm from a hard-coded
    36-id allow-list, never executing arbitrary code. Logistics: build_composite_impedance_field
    blends highway class/surface/damage/slope into one speed field feeding the existing
    speed_field parameter; optimize_delivery_route's stop ordering is now road-network-aware
    (not just the final drawn route) when a road network is given. Live-verified against real
    QGIS 4.2.2 throughout. See CHANGELOG.md's [1.7.0] entry for full detail.

v1.6.0 (2026-09-05):
    QA-gate/dataset-status state machine with geometry QA, P-code validation, and opt-in
    schema contracts; GDPR compliance remediation (bulk global-memory erasure + 4 further
    HIGH findings); the 791-task Humanitarian Mapping Task Register now drives the chat
    send path end to end; QGIS 4.x/Qt6 enum-compat generalized into one runtime resolver.
    Live-verified in a real QGIS 4.2.2 session (headless PyQGIS + a real chat-driven test):
    fixed a critical QgsColorRampShaderItem import crash that silently broke all 11 raster
    tools, and confirmed the print-layout disclaimer footer renders correctly. Also:
    ACLED/IMSMA-style humanitarian incident coding, road-snapped delivery routes, an
    auth-system-disabled diagnostic, and 4 execute_pyqgis_script sandbox bypasses closed.
    See CHANGELOG.md's [1.6.0] entry for full detail.

v1.4.2 (2026-08-22):
    ui/dock_widget.py's monolithic dock class split into CartogenAiDockWidget (outer dock:
    signals, header, tab wiring) plus separate ChatTabWidget/TasksTabWidget/HelpTabWidget
    classes, per docs/DOCK_WIDGET_SPLIT_PLAN_2026-08-21.md. Code moved verbatim, no logic
    changes. Verified via offscreen construction against a real QGIS Python environment
    (imports cleanly, all 3 tabs present, cross-tab signal wiring fires correctly) -- not yet
    verified in a real interactive QGIS session; see docs/RELEASE_SMOKE_TEST.md.

    Added the Cartogen Cloud Connect Gateway as a selectable (not yet functional) provider:
    CartogenClient now exported from agent/providers, wired into agent.py's provider
    selection and ui/settings_dialog.py's provider dropdown (placed last, not default --
    no gateway is deployed anywhere yet). Adds providers/cartogen.py's list_models(). This
    is client-side wiring only; see docs/PRO_TIER_BUILD_PLAN_2026-08-21.md for what's still
    needed before this tier is real (gateway deployment, billing, etc.).

    tests/test_export_tools.py's TestGenerateHtmlDashboardConnectivityNote now skips when
    folium is absent instead of failing, matching every other optional-dependency guard in
    this suite.

v1.4.1 (2026-08-21):
    Follow-up fixes after the v1.4.0 consolidation, from direct feedback: two living reference
    docs were still named/linked like leftovers from the old repo -- QGIS_AI_Agent_Feature_List.md
    and QGIS_AI_Agent_PRD.md renamed to CARTOGEN_AI_FEATURE_LIST.md/CARTOGEN_AI_PRD.md and fully
    rebranded (these are living docs, not historical logs, so unlike CHANGELOG.md's past entries
    this wasn't a rewrite-history concern). 44 broken file:// links hardcoded to the old machine's
    path (file:///c:/qgis_ai_assistant/...) converted to relative links. Updated every live
    reference to the renamed files across agent/scheduler.py, agent/tools/monitoring_tools.py,
    CLAUDE.md, CONTRIBUTING.md, plugin_upload.py's EXCLUDE_FILES, and docs/PRODUCT_TIERS.md,
    docs/HUMANITARIAN_GIS_FEATURE_REVIEW.md, docs/SAM_IMAGERY_EXTRACTION_SPEC.md. Left the
    genuinely dated/historical docs (CHANGELOG.md itself, STATUS_REVIEW_2026-08-20.md,
    SECURITY_AND_COMPETITIVE_REVIEW_2026-08.md) untouched, per this project's own frozen-doc
    convention -- they describe real past states, not leftover branding.

    Also added docs/OPEN_CORE_REPO_STRATEGY.md: this repo stays the single public Community
    codebase; Professional and Enterprise are decided (not yet built) to live in a separate
    private repo that one-way-syncs from this one via GitHub Actions, distributed from the
    project website with license-key gating. This resolves the licensing tension
    docs/TIER_RESTRUCTURE_PROPOSAL_2026-08-20.md flagged but didn't settle -- Community stays
    genuinely GPL v2/open in this repo; only the unbuilt Pro/Enterprise-only code would be
    proprietary, in the other repo. Added .github/workflows/sync-to-private.yml as an inactive
    scaffold for the sync mechanism (needs a real target repo before it can run).

    v1.4.0 (2026-08-21):
    Consolidated the dual-tree architecture into this single repo. Previously,
    C:\qgis_ai_assistant maintained two parallel plugin trees -- a root source
    tree (CartogenAiCore/cartogen_ai_core, renamed in v1.3.0) and a separately
    distributed "Cartogen AI/cartogen_ai/" copy (CartogenAi/cartogen_ai) kept
    in sync by build_cartogen_ai.py, so two QGIS plugin listings could exist
    side by side. This repo (C:\cartogen-ai) is the single, consolidated
    result: internal identifiers dropped the now-unnecessary "_core" suffix
    (CartogenAiCore -> CartogenAi, cartogen_ai_core -> cartogen_ai) since
    there is no longer a second tree to disambiguate from -- another QSettings
    key prefix change (cartogen_ai_core/... -> cartogen_ai/...), same
    breaking-change caveat as v1.3.0. build_cartogen_ai.py was retired (no
    sync target left). The 3 planned editions (Community/Pro/Enterprise, see
    docs/PRODUCT_TIERS.md) are reflected in documentation only -- nothing in
    this codebase is tier-gated yet, matching the project's existing honest-
    status convention. Added Claude Code / GitHub scaffolding: CLAUDE.md,
    .github/workflows/tests.yml, issue/PR templates. Full prior history
    (every version back to v0.2.0) remains in the old repo at
    C:\qgis_ai_assistant -- not carried over here, since this was a
    from-scratch copy/restructure rather than a git history migration.

    v1.3.0 (2026-08-21):
    Full internal rebrand of this root source tree from "QGIS AI Assistant"/QgisAiAgent/
    qgis_ai_agent to "Cartogen AI"/CartogenAiCore/cartogen_ai_core -- the second, separately
    distributed Cartogen AI/cartogen_ai/ copy tree already carried this branding and keeps its
    own identity unchanged. Deliberately a breaking change for existing installs: the QSettings
    key prefix moved from qgis_ai_agent/... to cartogen_ai_core/..., so anyone upgrading in
    place loses their saved API keys/provider selection and has to re-enter them once --
    confirmed with the project owner and accepted, not an oversight. Renamed: the plugin entry
    class (QgisAiAgent -> CartogenAiCore, plus QgisAiAgentDockWidget/
    QgisAiAgentSettingsDialog), the plugin module file (qgis_ai_agent.py -> cartogen_ai_core.py),
    the packaging PLUGIN_NAME (qgis_ai_assistant -> cartogen_ai_core), and every internal
    identifier across agent/, ui/, tests/, and the doc set (42 files). Left untouched, per
    CONTRIBUTING.md's existing "frozen historical doc" convention: QGIS_AI_Agent_Feature_List.md,
    QGIS_AI_Agent_PRD.md, IMPLEMENTATION_TASK_LIST.md, CHANGELOG.md's own past entries, and the
    dated STATUS_REVIEW/SECURITY_AND_COMPETITIVE_REVIEW/DOCK_WIDGET_SPLIT_PLAN/
    PROMPT_REFINEMENT_LAYER_SPEC docs. A post-hoc audit of the mechanical find-and-replace also
    caught and fixed: this changelog's own past entries and BUG_TRACKER.md's version-numbering
    note both had a real, currently-shipped filename accidentally rewritten by the same string
    pattern (restored by hand to match what's actually in dist/); and build_cartogen_ai.py's own
    PY_ONLY_RULES translation table had its search-pattern strings rewritten into no-ops by the
    same blind replace (fixed; also hardened build_cartogen_ai.py with the scratch-file exclude
    guard plugin_upload.py already had, and added CHANGELOG.md/BUG_TRACKER.md to its
    UNTRANSLATED_DOCS so neither can be retroactively mistranslated by a future sync run). 691
    tests, same known baseline, verified in both trees. The copy tree was not regenerated from
    source this round and keeps its existing branding/identity, per an explicit product decision;
    3 dated docs (DOCK_WIDGET_SPLIT_PLAN, PROMPT_REFINEMENT_LAYER_SPEC, STATUS_REVIEW) will stop
    getting their identifiers auto-translated into copy-tree branding on the next real sync run,
    since the translation table no longer recognizes the old identifier scheme they still use --
    flagged as an open question, not resolved here.

    v1.2.31 (2026-08-21):
    Round 2 of the ENGINEERING_PRODUCT_UX_REVIEW findings, final item: the dock_widget.py-split
    recommendation (REVIEW SS3.2), partially done and the rest deliberately deferred with a
    written plan -- see docs/DOCK_WIDGET_SPLIT_PLAN_2026-08-21.md for the full reasoning. Done:
    extracted _read_attached_file() (PDF/DOCX/CSV/XLSX/image/plain-text attachment parsing) into
    a new ui/attachments.py -- this function had zero Qt/QGIS dependencies despite living inside
    dock_widget.py, which imports qgis.PyQt/qgis.core unconditionally with no QGIS_AVAILABLE
    fallback and so cannot be imported or tested outside a real QGIS process at all. Moving it out
    gives it real test coverage for the first time (10 new tests in tests/test_attachments.py,
    including a real generated PDF and DOCX). NOT done: splitting QgisAiAgentDockWidget itself
    into ChatTabWidget/TasksTabWidget classes -- this sandbox cannot import, run, or visually
    verify a refactor of a live stateful Qt widget with cross-tab signal wiring and no existing
    tests, so a blind split risks shipping a broken dock panel behind a fully green (but
    non-representative) test suite. Documented the real coupling points found while reading the
    current structure (three tabs not two, shared statusSignal/usageSignal/receiveMessageSignal/
    toolStepSignal, one shared agent/task_manager reference) and a concrete plan for doing this in
    a real QGIS session instead. 691 tests total (was 681), same pre-known sandbox baseline as
    always. This closes out all four tasks from the ENGINEERING_PRODUCT_UX_REVIEW round-2
    follow-up work.

    v1.2.30 (2026-08-21):
    Round 2 of the ENGINEERING_PRODUCT_UX_REVIEW findings, continued: added session token-usage
    visibility to the chat UI (REVIEW SS3.2 -- "no cost/usage visibility in the UI despite real,
    documented cost-engineering work"). Every provider client's complete() now extracts a
    normalized {input_tokens, output_tokens} 'usage' dict from the raw API response when the
    provider actually reported one -- OpenRouter/OpenAI/Gemini/the Cartogen gateway stub share
    base.py's new extract_openai_style_usage() helper (all four go through an OpenAI-Chat-
    Completions-shaped endpoint); Claude's native input_tokens/output_tokens field names already
    match and are pulled through directly; Ollama reports under its own eval_count/
    prompt_eval_count field names, extracted separately. Never fabricated as zero when a
    provider/model omits usage -- the 'usage' key is simply absent from complete()'s return in
    that case, and QgisAiAgent.get_session_usage_text() returns None (not '0 tokens') until at
    least one real call has reported it, caveating the total explicitly when some calls did and
    others didn't. New QgisAiAgent.session_usage/_accumulate_usage()/get_session_usage_text()
    track a running session total (agent.run()'s tool-calling loop and the image-attachment
    direct-complete() path both feed it). ui/dock_widget.py gets a new usage_label below the
    existing status line, updated via a new thread-safe usageSignal after every turn (success or
    error) and after file/image attachment analysis. Deliberately no dollar-cost estimate --
    accurate per-model pricing across 5 providers needs a table that's guaranteed to go stale and
    mislead; token counts alone were the review's own suggested minimum bar. TestProviderReturnContract's
    strict shape check extended to allow the new optional 'usage' key without loosening what
    counts as a valid response shape otherwise. 10 new tests (usage extraction per provider, the
    honesty-preserving "omitted not zero" case, and agent-level accumulation/caveating). 681
    tests total (was 671), same pre-known sandbox baseline as always.

    v1.2.29 (2026-08-21):
    Round 2 of the ENGINEERING_PRODUCT_UX_REVIEW findings, continued: audited every destructive
    tool for preview/confirm safety-gate coverage (REVIEW SS3.2), per
    docs/DESTRUCTIVE_TOOLS_AUDIT_2026-08-21.md. Found and fixed a real inconsistency:
    calculate_area/calculate_length (vector_tools.py) called the exact same _add_calculated_field
    primitive as field_calculator -- an in-place edit of the live layer's attribute table -- but
    were not gated behind the confirmed=True flow field_calculator already requires. Both now
    follow field_calculator's exact PREVIEW_REQUIRED pattern. Also found load_project was already
    gated but SECURITY.md SS5's prose only named 2 of the then-3 gated tools -- corrected.
    Flagged, deliberately NOT fixed pending a product decision: 4 core humanitarian analysis
    tools (calculate_severity_index, calculate_presence_gap, calculate_population_in_need,
    calculate_damage_exposure_severity) perform the identical class of in-place mutation via a
    parallel _write_*_to_layer helper family in analysis_tools.py, but gating them trades
    consistency against inserting a confirmation click into the app's primary analytical
    workflow -- see the audit doc SS3 for the three options and why none was applied unilaterally.
    Reviewed and confirmed NOT gaps: execute_pyqgis_script (protected by its AST sandbox, a
    different and already-disclosed mitigation, not a silent omission), save_project (normal
    save semantics), save_workflow_preset (low-stakes settings overwrite), and the ~120 other
    tools that create new output layers rather than mutating existing ones. prompts.py rule 9
    and SECURITY.md SS5 updated to name all 5 now-gated tools. Also regenerated
    docs/TOOLS_REFERENCE.md, which turned out to be stale since before v1.2.25 -- it still listed
    4 tools removed in that release (export_attribute_table, generate_csv, generate_map_image,
    filter_features) and was missing the AI Imagery Feature Extraction and Monitoring &
    Scheduling sections entirely; both trees now regenerate identically (131 tools, confirmed via
    diff). 2 new tests (test_destructive_calculate_area_gate,
    test_destructive_calculate_length_gate). 671 tests total (was 669), same pre-known sandbox
    baseline as always.

    v1.2.28 (2026-08-21):
    Round 2 of implementing the ENGINEERING_PRODUCT_UX_REVIEW findings, per explicit user
    authorization to proceed with the remaining items (REVIEW §2.3, §3.2 x2, §4). This entry
    covers the first: gemini.py's auth-header inconsistency, now fixed rather than deferred.
    Fixed gemini.py's auth-header inconsistency (REVIEW §2.3): list_models() and
    grounded_search() both sent the API key via a '?key=' query parameter; GeminiClient.complete()'s
    main chat path already used the x-goog-api-key header. Query-string credentials are more likely
    to end up in server access logs, proxy logs, or browser history than a header. Before changing
    behavior, confirmed via WebSearch against Google's own current API docs (ai.google.dev/api) that
    x-goog-api-key is a real, documented, supported header for both the ListModels and
    generateContent REST endpoints this file calls -- not assumed. Both functions now use the header
    consistently. Added 2 new tests (test_gemini_list_models_uses_header_auth_not_query_param,
    test_gemini_grounded_search_uses_header_auth_not_query_param) asserting the header is sent and
    the query param is not, so a regression back to query-param auth would be caught. This sandbox
    still has no live network access to confirm the real API accepts it end-to-end -- still worth
    exercising via docs/RELEASE_SMOKE_TEST.md's Gemini provider row before the next release. 669
    tests total (was 667), same pre-known sandbox artifacts as always (1 DNS-lookup failure +
    6 scratch-file-permission errors, neither a real code defect).

    v1.2.25 (2026-08-20):
    Actioned 2 of the 3 open items from docs/STATUS_REVIEW_2026-08-20.md/docs/TIER_RESTRUCTURE_PROPOSAL_2026-08-20.md's
    engineering-gap list, plus one scaffold-only item. (1) Built apply_raster_stretch
    (agent/tools/raster_tools.py) -- closes the "no dedicated raster styling tool" gap: every
    raster-producing tool landed on the canvas with QGIS's raw unstretched default rendering.
    Auto-picks a diverging color ramp (QgsSingleBandPseudoColorRenderer) for NDVI/NDWI/NDRE-named
    layers, a grayscale min/max contrast stretch (QgsSingleBandGrayRenderer) otherwise. New
    ToolRouter alias, prompts.py rule 39, 16 new tests, calculate_ndvi/ndwi/ndre docstrings updated.
    (2) BREAKING CHANGE: removed 4 redundant duplicate tool registrations flagged but left
    unresolved in v1.2.20 -- generate_csv/export_attribute_table (pass-throughs to export_to_csv),
    generate_map_image (pass-through to print_map), filter_features (pass-through to run_query). Any
    saved workflow preset or script calling these 4 names by name will now fail -- no compatibility
    alias was kept. Updated the one internal reference (prompts.py rule 37 and
    docs/CVA_MARKET_ACCESS_RECIPE.md, both used filter_features by name) to run_query instead, same
    underlying behavior. Tool count: 134 -> 135 (item 1) -> 131 (item 2). Updated README.md,
    docs/PRODUCT_TIERS.md, docs/STATUS_REVIEW_2026-08-20.md's stale "134 tools" restatements. (3)
    Scaffolded agent/providers/cartogen.py -- a stub BaseAiProvider client for the not-yet-deployed
    Cartogen API gateway (service/gateway/'s LiteLLM proxy), modeled on openai.py's shape. Not
    imported by agent/providers/__init__.py or ui/settings_dialog.py -- confirmed via grep -- since
    the gateway backend doesn't exist yet either. Explicitly out of scope this round (unresolved GPL
    v2 licensing decision): no LICENSE/licensing-statement changes, no closed-source packaging, no
    tier/license gating in the plugin or settings UI, no service/website or service/gateway
    billing/budget changes. 655 tests total (was 639), same pre-known sandbox artifacts as always.

    v1.2.21 (2026-08-20):
    Full-codebase status review requested by the user. Found and fixed 3 small, real doc gaps
    while establishing the baseline: docs/TOOLS_REFERENCE.md's category grouping fell back to raw
    module names ("imagery_extraction", "monitoring_tools") for 2 of 16 categories instead of a
    readable label -- generate_tools_reference.py's label map was just missing those two entries,
    now "AI Imagery Feature Extraction"/"Monitoring & Scheduling". docs/PRODUCT_TIERS.md still said
    "all 125 tools" against the real, current 134. SECURITY.md's "Known limitations" section never
    mentioned that extract_features_from_imagery's FastSAM weights download is unpinned/unverified
    (delegated entirely to the ultralytics package's own download logic) -- added as an honest,
    accepted-risk bullet matching the section's existing disclosure pattern. See
    docs/STATUS_REVIEW_2026-08-20.md for the full architecture/tools/security/docs review and
    forward-looking plan.

    v1.2.20 (2026-08-20):
    Follow-up self-audit of v1.2.19's own changes, requested explicitly to find what wasn't caught
    the first pass. Found and fixed: rule 38 incorrectly told the model to apply_graduated_style on
    calculate_presence_gap's categorical field (needs apply_categorized_style) and omitted
    calculate_population_in_need entirely; calculate_ndvi/ndwi/ndre and
    calculate_raster_change_detection had near-zero router recall on realistic phrasing (thin
    original descriptions, now expanded, plus new aliases); a duplicate "load_3w_data" dict key
    introduced while fixing the above silently discarded its original aliases (caught by a full
    test run, not by inspection -- added an ast-based test so this class of bug can't ship silently
    again); calculate_ndvi/ndwi/ndre/raster_change_detection had zero test coverage, not even the
    baseline degrade-outside-QGIS check every other tool has. Also flagging, not yet fixed pending
    input: generate_csv/export_attribute_table are literal pass-through duplicates of export_to_csv,
    and generate_map_image/filter_features duplicate print_map/run_query the same way -- 4 fully
    redundant tool registrations adding router/token noise with no functional difference. 639 tests
    total, same pre-known sandbox artifacts as always.

    v1.2.19 (2026-08-20):
    Closed a real rendering gap and two routing/prompt gaps found while auditing the v1.2.17 tools:
    score_route_incident_risk's buffer layer is now auto-styled (translucent orange risk corridor,
    no border) instead of QGIS's default random single-symbol color; ToolRouter got alias entries
    for analyze_incident_trend/score_route_incident_risk (both previously had near-zero vocabulary
    overlap with realistic paraphrases like "is this area getting more dangerous", risking a miss
    from the top-40 candidate set); new prompt rule 38 tells the model to follow severity-scoring
    tools (calculate_severity_index/calculate_presence_gap/calculate_damage_exposure_severity) with
    apply_graduated_style so a computed score doesn't sit unstyled in the attribute table while the
    map is presented as done. docs/TOOLS_REFERENCE.md regenerated (134 tools, unchanged count).
    1 new regression test method plus 1 new table row added to tests/test_tool_router.py. 629
    tests total.

    v1.2.18 (2026-08-20):
    Wrote a JIAF multi-sector composite spec (grounded in JIAF 2.0's real Mosaic Method, researched
    before writing) plus two zero-code recipes: auto-reporting (corrects an earlier claim -- report
    tools aren't in the scheduler's allowlist) and CVA market-access (reuses population_access_gap,
    honestly scoped to what GIS can actually answer). No new tools shipped -- spec and docs only.

    v1.2.17 (2026-08-20):
    Implemented analyze_incident_trend (reuses forecast_trend's existing trend math over per-zone
    incident counts) and score_route_incident_risk (reuses buffer_analysis to score a route against
    nearby incidents). No-go zones ship as prompt guidance pointing at difference_layers, not a new
    tool, exactly as scoped. 134 tools total.

    v1.2.16 (2026-08-20):
    Wrote docs/ROUTE_RISK_AND_NOGO_ZONES_SPEC.md -- a design spec (not code) for time-windowed
    incident trend analysis, route-vs-incident risk scoring, and no-go zones as routing
    hard-excludes, composing hotspot_analysis/buffer_analysis/difference_layers/forecast_trend's
    existing trend math rather than new algorithms. No code changes.

    v1.2.15 (2026-08-20):
    Wrote docs/ROUTE_OPTIMIZATION_STRATEGY.md and docs/route_optimization_prototype.py -- a
    file:line-verified review of logistics_tools.py's routing accuracy (the fix: wire up
    SPEED_FIELD/DIRECTION_FIELD, already-supported params neither routing tool currently sets,
    instead of a flat 50 km/h DEFAULT_SPEED) plus a standalone OSMnx/NetworkX prototype. Strategy
    and prototype only -- no changes to logistics_tools.py itself. Also fixed a stale "125 tools"
    count in README.md (real count: 132).

    v1.2.14 (2026-08-17):
    Live-confirmed install failure on extract_features_from_imagery's dependency group: a Windows
    PermissionError on a locked markupsafe .pyd file when installing while QGIS is running (shared
    jinja2/markupsafe with folium). This is an OS-level file-lock, not fixable in plugin code -- added
    clear "close QGIS before installing" guidance to the tool's error message, requirements.txt, and
    README instead of leaving users to troubleshoot a raw traceback.

    v1.2.13 (2026-08-17):
    Declined the MCP server suggestion from the security/competitive review -- product owner does
    not want this surface added. No code was ever written toward it; marked as declined (not
    deleted) in docs/SECURITY_AND_COMPETITIVE_REVIEW_2026-08.md for the record.

    v1.2.12 (2026-08-17):
    Implemented extract_features_from_imagery: class-agnostic object-boundary extraction from a
    loaded raster via FastSAM, isolated in its own module (agent/tools/imagery_extraction.py) since
    it's the first tool needing a real ML runtime (torch) rather than an API-key provider. Runs
    locally/offline, CPU by default. Never labels results with a specific object class -- only a
    polygon and a confidence score (new prompt rule 34). NOT yet verified against a live model
    checkpoint -- structural/testable pieces only; see docs/SAM_IMAGERY_EXTRACTION_SPEC.md.

    v1.2.11 (2026-08-17):
    Wrote docs/SAM_IMAGERY_EXTRACTION_SPEC.md -- a design spec (not code) for imagery feature
    extraction via a SAM-family segmentation model (FastSAM recommended), following the same
    spec-before-build discipline as the Prompt Refinement Layer. No code changes.

    v1.2.10 (2026-08-17):
    Added docs/TRANSPARENCY_CARDS.md -- three procurement/compliance-facing one-pagers (running
    model-generated Python, running model-generated SQL, deleting/mutating data), grounded entirely
    in existing SECURITY.md file:line references. Documentation only, no code changes.

    v1.2.9 (2026-08-17):
    Added calculate_damage_exposure_severity: a RAPIDA-inspired composite combining before/after
    change detection, building-footprint exposure counts, and an optional hazard-intensity raster
    into one per-unit damage severity score, in the same thin-composite pattern as
    calculate_population_in_need. Scoped down from RAPIDA's full pipeline (no seismic modeling, no
    social signal ingestion).

    v1.2.8 (2026-08-17):
    Corrected repository/tracker/homepage to the actual repo, github.com/cartogenai-glitch/
    QGIS-CARTOGEN-AI (confirmed private) -- the previous commit used a name that turned out not to
    be the real one. Note: a private repo still conflicts with the QGIS Plugin Repository's
    requirement that metadata links be publicly accessible; not resolved by this change.

    v1.2.7 (2026-08-17):
    Repointed repository/tracker/homepage from github.com/baron-dev07/qgis_ai_assistant (pre-rebrand
    name, also an unrelated live listing's slug on the QGIS Plugin Repository) to
    github.com/cartogenai-glitch/cartogen_ai.

    v1.2.6 (2026-08-17):
    Hardened the recurring monitoring scheduler following a security review: a 1-minute interval
    floor (previously interval_minutes=0.001 became a ~60ms QTimer hammering the QGIS UI thread)
    and a 5-schedule concurrent cap. Documented both new features (recurring monitoring, prompt
    refinement) in SECURITY.md for the first time since they shipped.

    v1.2.5 (2026-08-16):
    Consolidated 4 redundant anti-fabrication prompt rules into one general principle plus
    one-line pointers (rules 12/20/23/32), added a new ask-vs-guess decision-hierarchy rule (33),
    fixed a stale hardcoded character count in prompt_refiner.py, and added a 12-case manual
    prompt-rule eval file to catch a rule getting silently weakened before it ships, instead of
    waiting for the next live bug report.

    v1.2.4 (2026-08-16):
    Clarified illustrative workflow-preset examples: a live Yemen monitoring session showed a
    concrete JSON preview with plausible-but-unconfirmed indicator field names next to a real,
    just-fetched layer name. Added a prompt rule requiring illustrative JSON examples to be
    explicitly labeled as placeholder templates, extending the existing ban on inventing
    real-world facts to illustrative examples.

    v1.2.3 (2026-08-16):
    Fixed a live bug caught on the first real use of recurring monitoring: the model could call
    schedule_recurring_workflow before ever saving the referenced preset, guaranteeing a failed
    tool call. Added prompt guidance to gather/confirm concrete inputs and save the preset first,
    plus a self-documenting error message with a ready-to-use save_workflow_preset example.

    v1.2.2 (2026-08-16):
    Added recurring monitoring workflows: schedule_recurring_workflow/run_monitoring_workflow
    re-run a saved read-only analysis workflow (severity index, presence gap, population in need,
    etc.) on a QTimer for as long as QGIS stays open, diffing per-unit results each tick and
    posting a short change summary into chat without spending an extra API call. In-session scope
    only -- no OS-level scheduler integration. Closes the PRD's "scheduled/recurring workflow
    object" gap.

    v1.2.1 (2026-08-16):
    Added calculate_population_in_need: composes calculate_severity_index's classification with
    estimate_population_exposure's zonal-statistics path to report total population in
    high-severity admin units -- the number allocation decisions actually get quoted in, not just
    a severity class. Closes section A of docs/HUMANITARIAN_GIS_FEATURE_REVIEW.md.

    v1.2.0 (2026-08-14):
    Humanitarian crisis-management and fund-allocation feature pass, from a field-analyst-
    perspective review of what already served that workflow and what genuinely didn't. Nine new
    tools, tool count 116 -> 125: calculate_severity_index (composite JIAF/INFORM-style needs
    index across admin units), population_access_gap (population beyond a facility's service-area
    reach), obfuscate_sensitive_points (geoprivacy jitter/grid-snap/admin-unit-snap for
    protection-sensitive point data), load_3w_data + calculate_presence_gap (3W/4W operational-
    presence loading and a severity-vs-presence coverage-gap overlay), fetch_hdx_admin_boundaries
    (OCHA COD-AB boundaries carrying real P-codes, since geoBoundaries doesn't publish any),
    generate_html_dashboard (interactive Leaflet situation map for non-QGIS audiences),
    generate_sector_coverage_report (beneficiaries reached vs. target, by sector), OCHA cluster
    color presets in the styling tools, and fetch_building_footprints (Microsoft Global ML
    Building Footprints, for baseline digitization).

    Every tool was checked against real code or live data before being built, not assumed from
    the review's own framing -- two items needed a different underlying approach than what was
    literally asked for and were corrected rather than built as scoped: P-code capture on
    fetch_geoboundaries turned out to be impossible (geoBoundaries publishes none), solved instead
    via a new HDX-based fetcher; imagery-based building/road extraction "via vision models" was
    reframed to a pre-computed dataset fetch instead, since asking a vision LLM for precise
    polygon coordinates is an unreliable pattern for work where geometric accuracy matters.

    Also new: a provenance/lineage footer on generate_report and generate_spatial_report,
    surfacing what tool/parameters/source produced each referenced layer -- for an audience that
    needs to check a spatial claim, not just take it on faith.

    v1.1.0 (2026-08-13):
    Interface modernization: live per-tool-call step indicators in chat ("Using X" / "Used X" /
    "Failed: X"), theme-adaptive QSS styling across the dock and Settings dialog, and a redesigned
    Settings dialog showing only the selected provider's fields instead of every provider's row
    always visible.

    Providers: dropped Groq, Cerebras, and DeepSeek. Five supported providers going forward:
    OpenRouter, Gemini, OpenAI, Claude, and local Ollama.

    Security/reliability pass (2026-08-14), following a full code review -- each item confirmed
    live before being fixed: the SSRF guard now re-validates every HTTP redirect hop, not just
    the initial URL (a local test server proved the old check could be bypassed via a redirect
    to an internal address), plus a download size cap; chat history is now opt-in (default off)
    instead of always saved into the shared project file; the false-success narrative backstop
    now scans every tool call in a turn instead of only the last one; the Stop button now
    actually works outside a real QGIS process; a status-callback leak on task failure is fixed.
    Also: a new build_cartogen_ai.py deterministically regenerates the second plugin tree from
    this one, replacing hand-maintained mirror scripts; release zips are no longer committed to
    git.

    v1.0.1 (2026-08-13):
    Production release v1.0.1: 116 PyQGIS tools, dual plugin builds (QGIS AI Assistant and Cartogen AI), bug fixes for Excel coordinate mapping, CSV encoding, multi-layer arrangement/opacity styling, and 13 new PyQGIS tools.

    Chat UX & response quality (user feedback round): chat bubbles now follow QGIS's
    actual live theme instead of hardcoded colors, with role badges, timestamps, and
    a markdown renderer upgrade (tables, blockquotes, strikethrough, nested lists);
    new prompt rules for structured answers, sensible-default gap-filling, and
    pre-finalization style self-review; new forecast_trend tool (pure-Python trend
    projection, always presented with a stated confidence, never as certain fact);
    new apply_graduated_symbol_style tool for proportional-circle point maps, closing
    the gap where the model hand-wrote ~40 lines of renderer code from scratch for
    this map type; fixed apply_categorized_style using a diverging ramp on unordered
    categories.

    Reliability: fixed a critical cross-thread signal bug where every tool call could
    silently fail with "Execution failed unexpectedly" even though the underlying
    operation had actually succeeded (also explains task plans getting stuck
    mid-progress); fixed the dock panel needing two clicks to open after a fresh
    install; added a Stop button to cancel an in-progress request; added a code-level
    check that catches the agent claiming success in its final answer when a tool call
    actually failed, instead of relying purely on prompt instructions.

    Security: hardened the PyQGIS script execution sandbox, including bypasses
    confirmed exploitable both by adversarial testing (import builtins; gc/inspect
    module access; string.format() attribute-traversal) and by real live use (Qt's
    own QFile/QDir/QProcess/QNetworkAccessManager/QSettings classes completely
    bypassed the Python-module blocklist since the module they live in must stay
    importable) -- see SECURITY.md for the full before/after testing table. Read-only
    SQL enforcement now also blocks server-side file-read and cross-server-execution
    functions (lo_export, pg_read_file, dblink) that a plain SELECT could otherwise
    reach undetected; made it fail closed instead of open. Added an SSRF guard on
    URLs the model can fetch (tested against IP-obfuscation and userinfo-confusion
    bypass attempts); flagged silent plaintext credential fallback in Settings; added
    prompt-level guidance against treating fetched web/OSM/HDX content as
    instructions.

    Efficiency: retry-with-backoff on every cloud provider; Claude prompt caching;
    batched geocoding plus caching for repeated geocode/OSM/HDX/boundary lookups; new
    installs default to complexity-based automatic model selection instead of always
    using the most expensive configured model.

    Data handling: attaching a CSV/Excel file now gives the agent the real file path,
    not just a row preview -- a new tool loads the FULL file as a real layer, with
    automatic point-geometry detection for coordinate columns.

    Providers: native web-search grounding and automatic model-retirement fallback for
    both Gemini and OpenAI; QGIS 4.0 (Qt6) compatibility fixes alongside continued QGIS
    3.x support.

    Also: Task Manager panel (progress bar, plan history, retry/edit-and-resend, memory
    search); in-app Help tab; multi-layer attribute join assistant; STAC imagery caching
    with a quota guard; bulk/single incident-point plotting tools.

# Tags are comma separated with spaces allowed
tags=python,ai,agent,openrouter,spatial,cartogen

homepage=https://github.com/http-solution/cartogen-ai-qgis-plugin
# category is left out on purpose: the cookbook allows only Vector, Raster, Database, Mesh or Web, and this plugin sits in the default Plugins menu.
icon=icon.png
# experimental flag
experimental=False

# deprecated flag (applies to the whole plugin, not just a single version)
deprecated=False

# Since QGIS 3.8, a comma separated list of plugins to be installed
# (or upgraded) can be specified.
# Check the documentation for more information.
# qpip reads requirements.txt (in this plugin's root) and offers the user a
# proper install dialog for the optional document-parsing/web-search
# dependencies, instead of this plugin running pip itself.
plugin_dependencies=qpip

# Category of the plugin: Raster, Vector, Database or Web
# category=

# If the plugin can run on QGIS Server.
server=False

