Changelog
All notable changes are documented here. Format based on Keep a Changelog. Versioning follows Semantic Versioning.
The authoritative version of this file lives at CHANGELOG.md in the repository — entries here are kept in sync with every release.
[Unreleased]
[2.2.3] — 2026-06-09
Added
- PMTiles tiling (
gispulse.tiling.write_pmtiles). GeoParquet → static PMTiles writer backed by DuckDBST_AsMVT, with the newtilingextra (pmtiles,pyarrow). Ports the lastmilou-branch capability into mainline. - VectorFileFetcher /
AccessProtocol.LOCAL_FILE. The core protocol roster can now read local vector files (KML/KMZ, GeoPackage, GeoJSON, Shapefile, FlatGeobuf, ...) throughgispulse.persistence.io.read_vector()and returns aSourceResultcarrying the materialized GeoDataFrame and CRS. This keeps MILOU-style local KMZ ingestion on the officialimagodata/gispulsepackage.
Fixed
- Line-volume memory corruption. Tile encoding ran one
ST_AsMVTquery per coverage tile on a single DuckDB connection; past a few hundred line features the spatial extension corrupted memory → non-deterministic segfault orST_AsMVTGeom: tile width and height must be positive. Rewritten as a single grouped query (features × tiles spatial join →GROUP BYtile). Robust and much faster. - Unsupported MVT property types. A
DATE/TIMESTAMP(or other non-numeric) column made tiling fail (ST_AsMVTaccepts only VARCHAR/FLOAT/DOUBLE/INTEGER/BIGINT/BOOLEAN). Properties are now coerced (wide ints → BIGINT, decimals → DOUBLE, everything else → VARCHAR).
[2.2.2] — 2026-06-07
Changed
snap_points_to_lineshardening.ref_id_colis now required: a missing column raises a clearValueErrorinstead of silently falling back to a positional index. Ties between equidistant lines break deterministically on the smallestedge_id(same inputs → same outputs). The unsnapped-row contract is unchanged (beyondmax_distance_m:snapped=False, nulledge_id/measure, original geometry kept, onlyoffset_distancereported) — downstream consumers (e.g. MILOU'sbuild_site_network_candidates) need no changes.
[2.2.1] — 2026-06-07
Added
- DuckDB-file datamarts (DP2b). A datamart can now be backed by a single DuckDB database file (
kind="duckdb") instead of one Parquet file per table:datamart://<mart>/<table>attaches the.duckdbfile read-only and selects the table, with bbox push-down viaST_Intersects.GISPULSE_DATAMARTSaccepts"kind": "duckdb".
[2.2.0] — 2026-06-07
Feature release adding a coherent set of generic GIS capabilities for network analysis, linear referencing and clustering (the "A–K" plan), plus a unified multi-source data provider. Fully backwards-compatible.
Added
- Unified data loader & providers (#374).
gispulse.load(source, …)/app.load()resolve files (incl. GeoParquet), remote URIs (s3/http),wfs:///stac:///ogc-features://, datamarts (datamart://, curated Parquet) and GeoNode instances (geonode://, read +publish()) to a GeoDataFrame or a lazy DuckDB scan. SpatialIndex(K) &NetworkGraph(F). Reusable core infrastructure: a thin STRtree wrapper (build once / query many) and a persistent routing handle building the NetworkX graph once and snapping points in O(log n). All network capabilities reuse it.build_network_graph(A),snap_points_to_lines(B),split_lines_at_points(C).planarize(D),connected_components(E).steiner_tree(G). Approximate minimum Steiner tree connecting a subset of terminals.cluster_network_dbscan(H). DBSCAN over shortest-path network distance.community_detection(I). Community partition (Louvain / greedy modularity / label propagation), tagscommunity_id.cluster_st_dbscan(J). Spatio-temporal DBSCAN (eps_m+eps_time).
[2.1.0] — 2026-06-05
Feature release consolidating everything that landed on main since 2.0.0: a wave of new declarative source plugins, the ELT push-down / manifest-v3 pipeline, S3 bulk materialisation for tabular sources, and an env-driven Garage object store. Fully backwards-compatible — the _compat meta-path shim and the PluginHub = ExtensionHub alias stay in place.
Added
- TABLE_FILE bulk materialisation to S3 (#358). The
AccessProtocol.TABLE_FILEfetcher can write a parsed DuckDB table scan to S3/Garage as Parquet (s3_uri/s3_key→COPY … TO 's3://…' (FORMAT PARQUET),MATERIALIZEmode), enabling national-scale tabular ingestion. - RestTableFetcher /
REST_TABLE(#337). Paginated tabular-JSON REST adapter for declarative sources. - New declarative source plugins: Géorisques over REST_TABLE (#338), protected areas — Natura 2000 + ZNIEFF (#341), SUP servitudes incl. ABF / PPR over WFS (#342), INSEE IRIS statistical units over WFS (#343), Cadastre Etalab bulk per département (#353), BDNB (#360), BODACC (#361), RNB (#362), loyers (#363), BAN (#364). Belgium: business parks (#367), Statbel statistical sectors + demographics (#368), GIPOD planned works / public domain — Flanders (#369).
milouclient (#366). S3/R2-backed DuckDB client with KMZ/XLSX ingestion in Lambert-72.- ELT push-down pipeline. SQL push-down for the 12 attribute capabilities (#264), aggregating / two-layer / CRS geometry caps (#296), dissolve + spatial_join (#297), nearest_neighbor + overlay (#298), temporal_filter + temporal_join (#299); manifest-v3 schema + loader + compiler +
gispulse migrate(#300), load-time cycle / ref validation (#301), manifest runner + view/table materialisation (#302),gispulse explainDAG inspection (#303), per-model data-quality asserts (#304). - Garage object store (#348). Env-driven S3 landing-zone service in compose.
- Changelog coverage gate (#335). CI guards that a version bump ships a matching changelog entry.
Fixed
_compatclass identity (#334). The legacy meta-path finder now prepends tosys.meta_pathand aliases legacy modules to the same object, soisinstance/pytest.raiseshold across thepersistence.*↔gispulse.persistence.*boundary (#333).- WfsFetcher registration (#355).
AccessProtocol.WFSnow resolves from the core roster. - src-dvf (#354). Re-sourced from the live geo-DVF CSV after the upstream parquet mirror was removed.
Changed
- Dependency bumps: starlette 1.0.1 (#365), redis
>=5.0,<9.0(#357), actions/checkout 6 (#351), actions/setup-python 6 (#350), actions/github-script 9 (#352). - Docs: Phase 2 feature pages (#323), changelog backfill + 2.0 migration guide (#322), manifest-v3 migration guide (#305), absolute repo links (#325).
[2.0.0] — 2026-05-20
The first major release. Numerically a jump from 1.6.2, but in practice the API surface bundles what was tagged internally 1.7.0, 1.8.0, and 1.9.0 — features that accumulated on main without ever being published to PyPI. We promote the whole stack in one tag and reset the public version to match the product story.
See MIGRATION-2.0 for the upgrade path. TL;DR: no application code change is strictly required — the _compat.py meta-path shim absorbs the import-path move and the PluginHub = ExtensionHub alias keeps existing imports working until 2.1.0.
Three threads converge here:
- Foundations (was tagged internally
v1.8.0) —gispulse.*mono-package,ExtensionHubreplacingPluginHub,GISPulseAppfaçade, full MCP server, data-pack regime, CLI / HTTP / template routers. - Worldwide aggregator (was tagged internally
v1.9.0) — lazy DuckDB-backed fetcher network covering 4 protocol families (GeoParquetS3,OGCFeatures,STAC,HttpFile) and a curatedworldwide_catalog.yml. - Data-pack rails — third-party data-packs can now ship on PyPI: a discovery channel via the
gispulse.data_packsentry-point, an Ed25519 signature gate on EXTERNAL manifests, and a shared licence payload format that also covers the future SaaS tenant licence.
Added
- Data-pack regime — PyPI discovery channel (T5). Third channel alongside the bundled OSS manifests and
GISPULSE_DATA_PACKS_DIR: a Python entry-point groupgispulse.data_packslets a third-party package register its manifests at install time. One bad pack never locks out the others. (#269) - Data-pack regime — Ed25519 signature gate (G1a).
DataPackManifestgains an optionalsignaturefield. EXTERNAL manifests carrying a signature are verified againstGISPULSE_DATA_PACK_PUBLIC_KEY; tampered or foreign-signed manifests are dropped with explicit log events. INTERNAL (bundled) manifests are exempt. SetGISPULSE_DATA_PACK_REQUIRE_SIGNATURE=trueto refuse unsigned EXTERNAL packs. (#271) - Unified Ed25519 licence payload format (L0). New
gispulse.core.licence_formatdefines the single payload schema shared by the per-machine licence key, the future SaaS tenant licence, and the data-pack manifest signature. Versioned viaschema_version, forward-compat, canonicalised JSON. (#266) - High-level OGC client for data packs (T1). New
gispulse.core.fetchers.ogc_client.fetch_features(...)— a one-liner over the consolidated transport with WFS vs OGC API Features dispatch and a typed network-error surface (OGCEndpointUnreachable,OGCClientError). (#267) - Declarative ZoningElement normaliser (T2). New
gispulse.core.zoning_normalizermaps heterogeneous source records into a common 8-field schema inspired by INSPIRE PlannedLandUse. CRS is mandatory and must be explicit (EPSG:XXXX). (#268) regulatory-zoningdata-pack content type (T3). New value inDATA_PACK_CONTENTS. NewRegulatoryZoningEntrydataclass +from_dict()validator: required-field set, no unknown fields, ISO-3166-1 alpha-2 country, known protocol, explicitEPSG:CRS, bbox 4-numbers. (#270)- Worldwide aggregator (EPIC #226). 15 sub-issues delivering lazy DuckDB-backed fetchers covering
GeoParquetS3,OGCFeatures,STAC,HttpFile, plus a curatedworldwide_catalog.yml(France / EU / world), HTTP endpoints (A10), portal Worldwide tab (A12). (#227-#241) - MCP server v1.8.0 (EPIC #206). 7 tools, dry-run mode, FS scoping. Stdio launcher via
gispulse mcp. (#202-#205, PR #242) gispulse.*mono-package consolidation (Foundations A). Flat 8-package tree → singlesrc/gispulse/package; ~280 OSS files moved with the_compat.pymeta-path shim preserving every old import root.GISPulseAppfaçade + 4 thin façades (Foundations B). Application layer over CLI / HTTP / MCP / template routers.ExtensionHubtwo-regime hub (Foundations C). ReplacesPluginHub, splits code plugins from data packs;DataPackManifest+templates/manifest.ymlfor the data-pack regime.- ELT push-down stack (EPIC #243). Dialect-aware SQL generation (#244, Lot 1), schema push-down (#245, Lot 2), per-capability push-down (Lots 3b-3e: geom multi-layer, dissolve/sjoin, nearest/overlay, temporal), unified manifest v3 (Lot 4A), cycle validation (Lot 4B), materialisation (Lot 4C),
gispulse explainDAG inspection (Lot 4E),assert:data-quality gates (Lot 4F), manifest v3 docs + ADR cross-refs (Lot 4G). 12 PRs merged ontomainon 2026-05-20. (#262, #264, #296-#305)
Changed
PluginHubrenamed toExtensionHub. Same module (gispulse.core.plugin_hub); aPluginHub = ExtensionHubalias preserves existing imports. Scheduled for removal in 2.1.0.gispulse.core.plugin_contractspublic surface frozen via__all__. The 8 symbols actually exported by the 1.6.2 wheel are gelled; types that moved toplugin_model.pywere never inplugin_contracts— no compat shim needed._compat.pydeprecation horizon corrected. Docstring andDeprecationWarningnow point at 2.1.0 instead of the stale "removed in 1.9.0" line.
Fixed
security-auditjob — silences two disputed upstream advisories (joblibPYSEC-2024-277,pyjwtPYSEC-2025-183) via--ignore-vulnallowlist with re-evaluation notes. No code change.
Migration
See MIGRATION-2.0. The summary:
- Legacy top-level imports (
core.*,capabilities.*,rules.*,orchestration.*,persistence.*,catalog.*) continue to work via the_compat.pymeta-path shim with a one-timeDeprecationWarning. PluginHubcontinues to work via thePluginHub = ExtensionHubalias.- Both shims will be removed in 2.1.0 — migrate to
gispulse.*/ExtensionHubat your leisure.
[1.7.0] — internal
Note:
1.7.0was never published to PyPI as a standalone tag — its scope is bundled inside2.0.0. The entry below documents what the tag would have contained for users tracking the EPIC #175 thread.
The "Wiring the ETL platform" release. EPIC #175 (PR #189) landed the unified plugin model as a skeleton; v1.7.0 made it work end to end — a data source can be declared, fetched over the network through a protocol registry, and watched for freshness so an external revision fires a trigger. GISPulse gains an "Extract" stage alongside its existing local-CDC triggers.
Added
- Unified plugin model +
PluginHub. Five plugin kinds (source,capability,sink,protocol,extension), entry-point discovery, and adiscover → resolve → gate → activatelifecycle with tier/trust gating. (EPIC #175, PR #189) source_changedtriggers. A trigger may declareon: {source_changed: <source>://<entry>, frequency: …}and fire when an external source publishes a new revision. (#195)SourceWatcherRegistrywired intogispulse watch. Polls each watched source'srevision()token at thefrequencycadence and dispatchessource.changedevents. (#197)- Core transport fetchers in the
ProtocolRegistry.WfsFetcher+OgcFeaturesFetcher(#192, PR #209),StacFetcher+RestGeoJsonFetcher(#192, PR #211). gispulse-src-cadastreandgispulse-src-ignsource plugins. Firstgispulse-src-*pilots — French cadastre (IGN Parcellaire Express) and IGN reference data (BD TOPO + ADMIN EXPRESS). (#184, #194)gispulse mcp. CLI launcher starting the GISPulse MCP server over stdio for LLM agents. (#201)- PostGIS dialect-drift scanner. Loader-time warning when a
run_sqlstring uses PostGIS-only constructs that will not run on the DuckDB-spatial contract dialect. (#146) - ETL documentation. Source Plugin Authoring Guide, "watch an external source" walkthrough (FR + EN),
source_changedsection inTRIGGERS_GUIDE.md. (#200)
Changed
- Catalog discovery consumes
PluginHub.records.catalog/registry.pyno longer runs its own scan — the hub owns the single scan./catalog/*is functionally unchanged. (#193) gispulse-src-cadastre.revision()is a real probe. Freshness read fromHTTP HEADETag/Last-Modifiedagainst the Géoplateforme WFSGetCapabilities. (#198)
Fixed
- SSRF guard on
ProtocolRegistry.dispatch_fetch(). Every fetch endpoint is validated through the sharedcore.ssrfguard before dispatch. (#199) test_p02file-lock flake. Known sqlite3 / pyogrio race markedflakyand retried viapytest-rerunfailures. (#191)
[1.6.2] — 2026-05-07
The "Format Frontier" release — DuckDB Spatial as the universal CDC substrate. Adds two new engines (spatialite, duckdb_diff), brings DML detection to seven file formats (GPKG, SpatiaLite, GeoJSON, FlatGeobuf, Shapefile, KML, CSV+WKT) — five of which had no native trigger surface — and closes EPIC #139 (DML semantics ADRs + WAL connection safety).
Added
- SpatiaLite engine. New
persistence.spatialite_engine.SpatiaLiteEngineshares the SQLite trigger DDL of GPKG but writes through pyogrio'sSQLite + SPATIALITE=YESdriver. Auto-routed for*.sqlite/*.dbURIs. (PR #151) is_spatialite_file(path)detection helper +bootstrap_spatialite_project(conn). Sibling to the GPKG bootstrap; shared_bootstrap_gispulse_internals(conn)helper. (PR #151)FileBlobChangeDetector. Reusable mtime + DuckDBST_Readsnapshot diff CDC. Hashmd5(ST_AsWKB(geom) || json_object(props))excludingOGC_FID. Snapshot persisted as<blob>.gispulse-snapshot.duckdb. Set-diff semantics: INSERT / DELETE only — UPDATE is undetectable without a stable PK. (PR #152)- Companion-file watching. Shapefile + MapInfo TAB watched via
max(mtime)across companion files; new_COMPANION_EXTENSIONSmap is extensible. (PR #152) DuckDBDiffEngine.SpatialEngineimplementation backed by the file-blob detector. GeoJSON, FlatGeobuf, Shapefile, KML, CSV+WKT. MatchesGeoPackageEngine.get_pending_changesshape soChangeLogWatcheriterates uniformly. (PR #152, #153)- Engine factory entries.
_spatialite_factoryand_duckdb_diff_factoryregistered as built-ins; URI inference maps suffixes automatically. (PRs #151, #152) persistence.gpkg_connection.connect_gpkg(path, …). Single entry point applying WAL +busy_timeout=5000on every GeoPackagesqlite3.connect. Migrated 8 scattered call sites. (#141, PR #145)- ADRs 0001-0004. DuckDB-spatial as the contract SQL dialect (#140 / PR #147), trigger cascade bounded fixed-point (#142 / PR #148),
_gispulse_change_logas a poll log (#143 / PR #150), DDL hooks out of scope (#144 / PR #150). - KML CDC, CSV+WKT CDC, MapInfo TAB companion files + pyogrio fallback. (EPIC #106 slices 1+2, PR #153, #154)
- Multi-engine
POST /datasets/{id}/enable_tracking. Route no longer hardcoded toGeoPackageEngine; resolves engine via URI suffix. SQLite-family installs AFTER triggers;duckdb_diffskips install (sidecar snapshot on first poll). (#157, PR #158)
Changed
bootstrap_gpkg_projectextracts a shared internal helper — regression test pins the GPKG path still produces a valid GeoPackage withapplication_id = 0x47504B47. (PR #151)
Documentation
docs/adr/0001 → 0004introduced underdocs/adr/; cross-linked fromarchitecture.md.dsl-sql-dialect.md— user-facing reference of the DSL SQL dialect contract.rules.mdcascade behaviour sub-section with tier table, two-layer explanation, link to ADR 0002. (PR #148)formats.md— SpatiaLite, GeoJSON, FlatGeobuf, Shapefile, KML, CSV+WKT, MapInfo TAB rows with CDC notes; new "CDC file-blob" section. (PRs #151-#154)walkthroughs/geojson-cdc.md(FR + EN) — fourth walkthrough end-to-end. (PRs #155, #156)
[1.6.1] — 2026-05-07
Same-day follow-up to v1.6.0. Closes the 3 deferred items from the v1.6.0 sprint kickoff in a single PR (#138) so the v1.6.x line ships its full promised surface — cross-source push-down, scalar lookup, and zero-config validate auto-wire.
Added
layer_lookup(layer, match, take, layer_geom)DSL fct. Scalar attribute lookup against a cross-source layer with three match modes (spatial_within,spatial_intersects, attribute-equality shorthand). Compiles to(SELECT _L."<take>" FROM "<layer>" AS _L WHERE <pred> LIMIT 1). (#124)- Cross-source layer registry.
gispulse.runtime.layer_registry.LayerRegistryATTACHes external GeoPackage / Parquet / PostgreSQL sources read-only and creates a DuckDB view per declared layer. (#122) - Top-level
layers:block intriggers.yaml. Declarative cross-source layer refs viaLayerSourceConfigModel. Duplicate-name guard at config-load time. (#122) build_runtimevalidate auto-wire. Newvalidate_rules,default_table,layer_sources,source_epsgkwargs wire aValidationRunnerdirectly onto the change-log watcher.- Per-rule
table:and top-leveldefault_table:. Resolution order:rule.table>default_table> GPKG single-table autodetect >ValidationTableResolutionError.
Changed
compile_validate_rulesaccepts atable_resolvercallable — supports per-rule resolution. Legacytable=parameter preserved for v1.6.0 callers.
[1.6.0] — 2026-05-07
The "DuckDB Spatial Inside" release. Closes EPIC #104 — a one-day cascade of 7 PRs (#129 → #135) lands the foundation, the DSL geom function whitelist, granular DML verbs, the declarative validate: block end-to-end, and the long-standing B-08 DELETE predicate gap.
DuckDB spatial moves from "embedded if you opt in" to the universal compute substrate: new DSL geom functions compile to DuckDB SQL, the validation runner evaluates rules through a DuckDB ATTACH on the GeoPackage, and Atlas R1 bench against pyogrio justifies the pivot — DuckDB COPY is 2.3× to 3.6× faster than pyogrio on 1M EPSG:2154 polygons, peak RSS divided by ~3.4×.
Added
- DuckDB spatial extension — lazy install on first use.
gispulse.runtime.duckdb_engine.get_spatial_connection()runsINSTALL spatial; LOAD spatial;on first call.DuckDBSpatialUnavailablesurfaces air-gapped failures explicitly. (#113, PR #129) gispulse doctor --install-spatial. Pre-installs spatial extension and probes a curated set of EPSG roundtrips (EPSG:4326 / 3857 / 2154 / 27572) against apyprojbaseline. (#114, PR #129)- Engine inference from the dataset URI.
triggers.yamlno longer requires explicitengine::*.gpkg→gpkg,postgresql://...→postgis,*.shp / *.geojson / *.fgb→duckdb_diff. (#115, PR #129) - DSL geom functions — first whitelist. Seven safe push-down functions:
geom_area_m2,geom_perimeter_m,geom_length_m,geom_centroid_x,geom_centroid_y,geom_npoints,geom_is_valid. Auto-projects toEPSG:2154by default. (#116, #117) - DSL expression parser — safe-by-construction. AST walked under strict allowlist (literals, column refs,
+ - * / %, parens). Boolean mode unlocks== != <= >= and or notforvalidate:rules. (#118) when:granular DML verbs.INSERT,UPDATE_GEOM,UPDATE_ATTR,DELETE,BULK. The watcher resolves a coarseUPDATEto its granular variant via the change-log'sgeom_changedflag. (#119)geom_changedflag in thedml.changedpayload. Subscribers can render geometry edits differently from attribute edits. (#120)validate:top-level block intriggers.yaml. Declarative validation rules withmode: warnormode: tag. Rules compile at config load. (#121)tag_field:action. Writes status (and optional message) onto the row, auto-creating target columns viaPRAGMA table_info+ALTER TABLE ADD COLUMN. Shared handler powers both explicit YAML actions and thevalidate: mode: tagbridge. (#123)- DSL cross-layer subquery functions.
geom_within(layer='communes', match='code_insee')andgeom_overlaps_any(layer='self', exclude_self=True). Compiler emitsEXISTS (SELECT 1 FROM "<layer>" AS _L WHERE …)with strict identifier validation. (#122) ValidationRunner+make_gpkg_sql_evaluator(gpkg_path). Engine-agnostic runtime component compiling each rule once at boot, evaluating per row through an injectedsql_evaluator. Broadcastsvalidation.failedon the event hub. Per-rule isolation: a single bad rule never aborts the batch. (PRs #132-#133)ChangeLogWatchervalidation hook. When aValidationRunneris injected, every INSERT / UPDATE_GEOM / UPDATE_ATTR drivesrunner.evaluate(...). (PR #133)- ESRI Attribute Rules vocabulary aliases.
kind: constraint | calculation | validationaccepted as cosmetic aliases ontriggers.yaml. (#125) - New docs pages.
dsl-geom-functions.md,dsl-validation.md,migration-from-esri.md, v1.6.0 section onengines.md. (#126)
Fixed
- B-08 — DELETE predicates can finally filter on the row's pre-delete state. AFTER DELETE trigger writes
OLD.*asjson_object(NEW.*)intoold_valuessince v1, but the changelog reader's tail whitelist dropped the column. Whitelist now includesold_values; the watcher hydratesChangeRecord.old_valueswhen at least one active trigger carries a predicate AST. No GPKG migration. (#120, PR #135)
Security
dml.changedbroadcast payload stays minimal on DELETE. Row attributes captured by AFTER DELETE are exposed only to the internal predicate evaluator, never on/ws/events. Testtest_dml_changed_does_not_leak_old_valuespins the contract.validate:rule SQL is never spliced raw. Strict[A-Za-z_][A-Za-z0-9_]{0,62}validator on every identifier; literals SQL-quoted; AST parser refuses any node outside the allowlist.
Performance
DuckDB COPY GDAL/GPKG is now the bulk write-back fast path. Atlas R1 bench on 1M EPSG:2154 polygons (median of 3 runs):
Scenario pyogrio (s) DuckDB COPY (s) Speedup RSS pyogrio RSS DuckDB Append +100k 8.19 3.63 2.26× 950 MB 273 MB Update attribute 6.94 2.75 2.52× 839 MB 255 MB Update geometry 8.87 2.47 3.59× 843 MB 275 MB Fallback to pyogrio remains forced for datasets > 5M rows, GPKG with custom triggers / views, and append-in-place semantics.
[1.5.3] — 2026-05-05
Hotfix release for EPIC #103 — 4 P0 bugs identified by Beta on the v1.5.2 DML triggers + QGIS workflow.
Fixed
- B-05 — QGIS layer names with spaces, accents or dashes are accepted. Validator now delegates to
core.sql_safety.validate_layer_name()accepting any character safe inside quoted identifiers; only",',;,\and control chars rejected. Trigger object names go throughslug_identifier(). (#107) - B-02 — SET_FIELD trigger no longer loops infinitely. Origin-tagging M1: tracked layers grow a
_gispulse_origin TEXTsentinel (schema v3 migration, idempotent on re-bootstrap). AFTER UPDATE trigger gains a WHEN clause suppressing re-fires when the row carries atrigger:<id>marker. (#108) - B-01 — Bulk threshold Mode 3 (bulk WS event + per-row trigger eval). New
bulk_eval: Literal["skip", "per_row"] = "skip"constructor parameter."per_row"emits onebulk.changedsummary AND evaluates triggers per row. (#109) - B-13 — Schema drift watchdog rebuilds triggers on column changes. Wall-clock-throttled drift check (default 5 s) re-hashes
PRAGMA table_info; on mismatch drops + re-installs change tracking and broadcastsschema.changed. First sighting is silent. (#110) - CI —
_drop_rtree_triggersand_connect_with_retryhardened. Retry helper budget bumped from 8×0.15 s to 20×0.25 s.
Notes
- Schema bump v2 → v3. Existing v2 GPKGs upgrade in place on the next
bootstrap_gpkg_projectcall (engine boot), idempotent. bulk_eval="per_row"is opt-in on the watcher constructor.- Schema-drift watchdog runs by default at 5 s; set
schema_drift_check_interval_s=0to disable.
[1.5.2] — 2026-05-04
Big-launch release. Runtime keeps the v1.5 surface; adds the QGIS plugin, three end-to-end walkthroughs, plugs a critical portal-mode middleware gap, and lands /system/doctor.
Added
- QGIS plugin (
qgis_plugin/). Thin dock widget shelling out to systemgispulseCLI viaQProcess. Version-gate (≥1.5.0), OS-specific install dialog, attach-trigger combo (vector layers only), non-blocking runner with streamed coloured logs + Cancel, post-run change summary + auto-reload + 5-min Restore. ~500 KB unzipped, 99 tests, lockstep version with the wheel. (#71, #73, #74, #76, #78, #80, #84) - Walkthroughs (FR + EN).
classify_buildings_in_isochrones,recompute_isochrones,log_event. (#89) POST /system/doctor. Backend health endpoint mirroringgispulse track doctor. Closes #91. (#97)- CI —
build-plugin-zipjob packaging and verifying the plugin ZIP on every tag.release.ymldouble-gated. (#79)
Fixed
- Security —
ProductionAuthMiddlewarewas never mounted in portal mode.PluginHubmiddleware install was nested inside theis_portal=Falsebranch ofcreate_app, so the enterprise auth middleware (shipped viagispulse.middlewareentry-point) was never installed whengispulse portalran.GISPULSE_ENV=productionportal deployments were UNPROTECTED on/filter/*,/ogc/*,/ws/*. Hoisted thehub.middlewareinstall loop above theis_portalbranch. Closes part 2 of #87. (#96) - CI —
test_p02_enable_tracking_full_lifecycleflake on Python 3.10/3.12. Wrappedsqlite3.connect()with 3-attempt retry. (#86, #57) - Docs — dead
git cloneURL in QGIS plugin install guide. Pointed togithub.com/gispulse/gispulse(404); actual repo atgithub.com/imagodata/gispulse. Fixed FR + EN. (#101)
Changed
release.yml—github-releasewaits for bothpublish-pypiandbuild-plugin-zip.
Security
- Dependencies bump:
docker/build-push-action6 → 7,actions/upload-pages-artifact4 → 5,actions/upload-artifact4 → 7. (#98-#100)
[1.5.1] — 2026-04-30
Mode 2 portail Community: GISPulse now ships a local visual workbench. pip install gispulse-portal adds the bundled SPA to your CLI install; gispulse portal opens http://localhost:8001/portal with same-origin engine.
Added
gispulse portalCLI command mounting the bundledgispulse-portalSPA on/portalvia FastAPIStaticFiles.--port,--no-browser,--backend=URL,--devflags./api/examples/*mini-backend — read-only registry of bundled GPKG fixtures (muret-parcels,muret-flood-zones,toulouse-isochrones,bordeaux-rpg) for the public "Try it" demo. Hard-capped (5 s timeout, 1000 DML records, 50 triggers, 50 MB tile cache);DryRunDispatchercaptures actions but never executes side-effects.- Docs — "Running the portal locally" + "Running the engine" guides (FR + EN).
- CLI ↔ Portal symmetry matrix (
guide/symmetry.md) — 82 capabilities mapped row-by-row, 31 ⚠️ asymmetries logged for v1.6+ triage.
Companion release
gispulse-portal 1.5.1ships on PyPI for the first time. The wheel bundles the built VitePress SPA sogispulse portalcan serve it same-origin on localhost.
Fixed
cli.pyengine -e/--enginehelp string now mentionshybridalongsideduckdbandpostgis.
[1.5.0] — 2026-04-30
QML-grade styling release: load, classify server-side, edit, and export QGIS-compatible styles end-to-end.
Added
POST /datasets/{id}/layers/{layer}/breaks— server-side classification (quantile, equal-interval, Jenks, std-dev, pretty) wrappingClassifyCapability.PUT /datasets/{id}/styles— persistLayerStyleDefto the GPKGlayer_stylestable.POST /datasets/{id}/styles/import— multipart.qmlupload, parsed viapersistence/style_converter.pyand persisted.- QML roundtrip integration suite — 5 representative fixtures (single, categorized, graduated, rule-based, labels) tested in CI to guard against lossy export/import cycles.
Changed
- Style classification moves to server-side by default; client falls back locally for offline scenarios.
persistence/style_converter.py(~608 LOC) becomes the source of truth for QML ↔LayerStyleDef. GeoStyler bridge dropped.
[1.3.1] — 2026-04-29
Hotfix unblocking the v1.3.0 distribution: pipx install gispulse now ships a working triggers run / watch, the local Docker stack boots on community tier, the portal serves favicon/robots/manifest correctly, CI is green again.
Fixed
- Packaging —
httpxcore runtime dependency — moved from[api]/[sso]/[dev]extras into base.pipx install gispulsepreviously produced a CLI fortrack/info/runbutgispulse triggers runandgispulse watchcrashed onModuleNotFoundError: No module named 'httpx'. Workaround for 1.3.0:pipx install "gispulse[api]". - Packaging —
pyarrowcore runtime dependency — declaredpyarrow>=14,<22in base. Without it,gispulse run --output result.parquet, the GeoParquet writer, and any DuckDB pipeline that lands GeoParquet viaCOPY ... TO ... (FORMAT 'parquet')crashed withImportError: Missing optional dependency 'pyarrow.parquet'. - Runtime —
gispulse watch --bulk-thresholdcrashed at startup —cli_watch.pywired--bulk-thresholdstraight intobuild_runtime(bulk_threshold=...), butbuild_runtime()never accepted the kwarg. - API — pipelines
ref_layer500 —/pipelines/execute-stepsresolved aliases but left the original keys inparams. Fixed viadict.pop()to strip plumbing keys before the capability call. - API — OSS auth stubs + websockets —
/api/auth/providersand/api/auth/menow ship OSS stubs returning[]/200 null. Switched the[api]extra touvicorn[standard]so/ws/eventsupgrades stop failing withNo supported WebSocket library detected. - API — SPA root static assets — the fallback now tries the dist root before applying the SPA-route whitelist + index.html fallback.
- Compose — community-tier boot —
docker-compose.local.ymlno longer hardcodesGISPULSE_ENGINE=postgis; PostGIS opt-in via--profile postgis. - Catalog — IGN Scan 25 dead entries — IGN Géoplateforme deprecated
GEOGRAPHICALGRIDSYSTEMS.MAPS. Droppedbasemap:ign-scan25andign-scan25-wmts;GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2exposed asbasemap:ign-plan/ign-plan-wmts.
Changed
- CI —
testjob installs[dev,api,postgis,mcp,raster,network,classification,pointcloud,scheduling,sso]extras instead of[dev]alone. - CI —
pip-auditignoresCVE-2026-3219(pip 26.x tar/ZIP confusion, no upstream fix yet; re-evaluate quarterly). - Docs — README pipx quickstart aligned with v1.3 CLI surface.
Security
- Dependencies — bump
fastmcp>=0.1,<2.0→>=2.14.2,<4.0(CVE-2025-62800 / 62801 / 69196 / 64340 / 2026-27124 / GHSA-rcfx-77hg-w2wv). - Dev — bump
pytest>=7.0,<9.0→>=9.0.3,<10.0(CVE-2025-71176).
[1.3.0] — 2026-04-27
The "no plugin required" CLI release — gispulse track + gispulse watch make any QGIS / ogr2ogr / FME / ArcGIS / DBeaver writer a first-class trigger source.
Added
gispulse track— SQL change-tracking subcommand (install/uninstall/list/tail/doctor [--auto-fix]). Installs_gispulse_change_logtriggers on a GPKG so any client can write to the file and the daemon picks up the changes. (#4, #6)gispulse watch— top-level foreground daemon. SIGINT/SIGTERM clean shutdown (2 s drain), 60 s structured stderr heartbeat, repeatable--webhook hostallowlist override. Supports daemon mode and--oncedrain. (#5, #11)- Trigger payload v2 —
_gispulse_change_logSQLite triggers bakenew_values/old_valuesJSON columns + ageom_changedflag, captured atomically inside the SQLite trigger viajson_object(NEW.*). Removes the post-commit_load_row_values()SELECT. (#7) - Bulk-mode tick —
--bulk-threshold Ncollapses ticks withN+rows into a singlebulk.changedsummary event instead of broadcasting per-row. (#8) - Packaging —
packaging/systemd/gispulse-watch@.service+packaging/docker/Dockerfile.watch+docker-compose.watch.yml. (#9)
Notes
- Closes the Mode 1 scope of #2 entirely. Mode 2 (portal trigger CRUD) remains on the roadmap.
gispulse triggers run --watchand the new top-levelgispulse watchcoexist for one release.- CI baseline cleanup (#19) — dropped removed
pip-audit --fix-auto=offflag, regenerated capability matrix, ruff drift cleared (514 → 0 errors), workflows aligned ongispulse-portalsibling-repo split.
[1.2.1] — internal
Note:
1.2.1was never published to PyPI as a standalone tag — its scope was rolled into1.3.0. The entry below documents what the tag would have contained.
Added
gispulse triggers— new CLI subcommand group (run/validate/list) for the standalone trigger runtime (Mode 1). YAML config → GPKG DML triggers, no FastAPI process required.gispulse/runtime/headless_runtime.py—HeadlessRuntimewiresChangeLogWatcher+TriggerEvaluator+ActionDispatcheragainst aNullEventHubso the ESB pipeline runs outside the FastAPI lifespan.gispulse/runtime/config_loader.py— strict pydantic v2 schema (extra="forbid",yaml.safe_loadonly, path-traversal guard).gispulse/runtime/predicate_dsl.py— hand-written LL(1) recursive-descent parser for thepredicate:field. Noeval, nosimpleeval, no third-party dep. Operators:== != > >= < <= AND OR NOT IN NOT IN IS NULL IS NOT NULL.MAX_DEPTH=32.gispulse/runtime/sqlite_retry.py—RetryingSqlExecutorwrapsGeoPackageEngine.execute()with exponential backoff onSQLITE_BUSY. Caps at 5 retries / 30 s total.persistence/sql_guardrails.py—enforce()is the single sandbox between YAMLrun_sql/set_fieldactions and SQLite. AllowlistINSERT/UPDATE/DELETE/SELECTonly. Hard-blocksATTACH/DETACH/PRAGMA/VACUUM/LOAD_EXTENSION/writable_schema/sqlite_master. Multi-statement payloads refused.
[1.2.0] — 2026-04-25
First public AGPL-3.0 release on PyPI as gispulse. Source: https://github.com/imagodata/gispulse.
Added
- PluginHub + plugin contracts —
core/plugin_hub.py+core/plugin_contracts.pyfor plugin discovery via Python entry-points, six groups (gispulse.routers,gispulse.middleware,gispulse.auth_provider,gispulse.billing_provider,gispulse.licence_provider,gispulse.connectors). - Pricing catalog —
core/pricing_catalog.jsonfor the tier→features catalog (community / pro / team / enterprise) withinheritschain. teamtier inpersistence.tier.VALID_TIERSandcore.config.EngineSettings, betweenproandenterprise.- Multi-project gate on
POST /projects(community=1, pro=5, team+=∞). - Pro-tier gate on
triggers_router(router-level) andpipelines_router(/execute,/execute-steps).
Changed
- Repository layout — proprietary modules (Stripe billing, OIDC SSO, RBAC admin, production auth middleware, licence Stripe sync) moved to a private companion package
gispulse-enterprisedistributed under a commercial EULA. The OSS engine ships only AGPL components and discovers enterprise via entry-points at runtime. gispulse/adapters/http/app.py— billing, auth, admin router mounting now driven byPluginHubdiscovery instead of hard-coded imports; degrades cleanly when no enterprise plugin is installed.
Removed
gispulse/adapters/billing/,gispulse/adapters/http/oidc.py,middleware/production_auth.py,routers/{auth,billing,admin}_router.py— moved togispulse-enterprise.pricing.yml(EUR amounts, early-adopter terms) — moved togispulse-enterprise/config/pricing_commercial.yml. The technical tier→features mapping stays here ascore/pricing_catalog.json.- Test files specific to enterprise modules.
[1.1.1] — 2026-04-25
Added
capabilities/vector/— the monolithicvector.py(4,359 LOC, 43 capabilities) was split into a 32-module per-domain package. The public surface is preserved through a re-export shim; everyfrom capabilities.vector import ...keeps working unchanged.
Changed
gispulse/__init__.py— fallback__version__changed from hardcoded"1.0.0"to"unknown"whenimportlib.metadatais unavailable.portal/package.json+docs-site/package.json— versions synced to1.1.1to matchpyproject.toml.
Fixed
- Accessibility — keyboard navigation on
PipelinePanel, portal imports unified around design-system tokens.
[1.1.0] — 2026-04-25
Added
- Playground scenarios — S5 Park accessibility (Versailles, BD TOPO vegetation ≥ 1 ha +
nearest_neighbor+classify, weekly cron) and S6 Price-per-m² DVF map (8 steps, 50 m fishnet, YlOrRd quintiles). - Capabilities — classification & stats —
head_tail_breaks(Jiang 2013),normalize(log1p / minmax / zscore),grid_create,hexgrid_create,spatial_aggregate,classify_categorical,bivariate_choropleth,graduated_size,continuous_ramp,kde_heatmap. Clustering:cluster_kmeans,cluster_dbscan,cluster_hdbscan,morans_i,getis_ord_g,nearest_neighbor,od_matrix,spatial_weights. - Capabilities — 3D pointcloud — LAS / LAZ sprint:
pointcloud_load_las,pointcloud_filter_classification,pointcloud_zonal_height,pointcloud_grid_summary. - Capabilities — layer manipulation P0-P3 — overlay (
overlay_intersection,overlay_union,erase), selection (sort,deduplicate,random_sample,top_n), shape ops, transforms (affine_transform,swap_xy,reverse_lines), Z/M (add_z,drop_z,add_m,drop_m), pivot/unpivot,classify_by_ring,merge_layers, attribute logic (add_field,drop_field,select_columns,rename_field,cast_field,attribute_join,lookup_table,coalesce_fields,case_when), temporal (temporal_filter,temporal_join). - Playground UX — rubber-band drawing with snap-to-close + keyboard shortcuts + live measurement; client-side polygon intersection styling (S4 road-setback).
- DVF Etalab 2022-2024 — sample dataset bundled with
examples/prepare_playground_data.py --city versailles(dvf_venteslayer). - Style sidecars —
.style.qml/.style.sld/.legend.jsonfiles emitted next to vector outputs for direct QGIS / GeoServer import. - SQL preview — explicit auth gate + capability blocklist on the PostGIS SQL capability.
Changed
core/config.py— centralised all environment variables into a single Pydantic Settings module (13 groups:engine,database,storage,s3,api,oidc,session,redis,logging,audit,stripe,telemetry,jobs). Backward-compatible with every existingGISPULSE_*name.- Default engine — changed from
duckdbtogpkg(portable GPKG / GeoPandas mode). - Removed scattered
os.environ.get()calls — routers, adapters, persistence: everything routes throughsettings. - Playground S5 rewritten as park accessibility per building.
- Playground S6 extended to a 250 m then tightened to a 50 m fishnet choropleth.
- Playground S3 — 6-step pipeline collapsed to 3 via
cost_budgets+classify_by_ring. adapters/http— namespace fork resolved: legacy tree deleted, prod entrypoints flipped togispulse.adapters.http.app.- Security —
MD5replaced byBLAKE2b,evalsandboxed fornp,_ensure_validrestored.
Fixed
- Capabilities — 4 P0 closed:
force_geometry_type,attribute_joinon a plain DataFrame, NaN crash inadd_z/add_mfrom_column,singleparts_to_multipartsilent data loss on mixed geom types. - Capabilities — pointcloud grid 2D NaN, KDE grid blow-up,
CalculateRCE sandbox. - Tests — repaired 27 tests once CI was unblocked, removed shadow
__init__.py, enabledasyncio_mode = "auto", fixedworkflows/ftth_network_analysis.pySyntaxError. 3,600+ tests green. - Tests — isolate
GISPULSE_ENGINEmutations; conftest auth-disabled-by-default. - Billing — default
StripeSettings+ actionable error messages when Stripe keys are missing. - Capabilities —
clip/intersectsno longer evaluateGeoDataFrametruthiness;spatial_predicatefallback made explicit. - Playground — S6
drop_price_outliersrenamed todrop_value_outliers(filters the rawvaleur_fonciere, not price-per-m²). - i18n —
PipelinePanelstrings; default-engine alignment; pipelinesref_layersplural. - Performance — lazy-loaded
DualMapView. - Rules router — payload validation before persisting (400 with structured errors).
[1.0.2] — Sprint S1→S6 (2026-04-12)
Six sprints of audit and hardening: security, architecture, tests, observability, router coverage, Prometheus metrics.
Added
Architecture — Declarative Grammar v2 (Sprint S1)
PipelineSpec/StepSpec/TriggerSpec— unified grammar replacing 3 divergent DSLs- DAG support — steps can reference other steps via
step.input - Conditional steps —
step.whenpredicate evaluation on current GeoDataFrame - Inline triggers —
on/when/thensyntax within pipelines - Backward-compatible — v1 flat rule lists auto-converted to v2
PipelineExecutor— unified executor (linear and DAG mode viaGraphExecutor)PluginRegistry[T]— generic thread-safe registry with entry point discovery
Pipeline v2 API (Sprint S2)
POST /api/pipelines/execute— execute v2 pipelines withPipelineSpecJSONPOST /api/pipelines/validate— dry-run pipeline validationGET /api/pipelines/examples— v2 pipeline examples- CRUD
/api/triggers/{id}/operations— spatial operations persistence in triggers SessionManager.run_pipeline_v2()— native delegation toPipelineExecutor- TypedDict for 10 capabilities —
FilterParams,BufferParams, etc. - PipelineEditor — portal editor mode: import/export v2 JSON, execute via
/pipelines/execute
Portal — Decomposition & WebSocket (Sprint S3)
LayerItemButtonandDatasetItemextracted fromLeftPanel.tsx(1183→774 lines)- WebSocket listener replaces
setIntervalpolling intransformStore - CI GitHub Actions —
ci.ymlworkflow with backend (pytest, ruff) and frontend (tsc, vite build) jobs
Documentation & Tooling (Sprint S4)
scripts/export_openapi.py— auto-generatesdocs/openapi.json+docs/API_REFERENCE.md- QUICKSTART.md, RULES_GUIDE.md, TRIGGERS_GUIDE.md, API_QUICKSTART.md — 4 user guides
docs/openapi.json— complete OpenAPI 3.1 specification (88 endpoints)
Changed
Models (Sprint S1)
core/models.pysplit (795→280L) into 6 modules:enums.py,conditions.py,predicates.py,graph.py,relations.py,session.pyRule.orderextracted from config bag to dedicated field
Portal (Sprint S3)
- Predicate type renaming — removed
*Nodesuffix (AttrPredicateNode→AttrPredicate) - Forge operations connected —
OperationExecutor→ ESB:RUN_SQLactions run end-to-end
Removed
- Non-functional client stubs —
clients/qgis/,clients/arcgis/,clients/desktop/(code in git history) - ESB
CircuitBreakerandDeadLetterQueuemarkedEXPERIMENTAL, lazy-import only
Security (Sprint S1)
- Patch for 13 critical vulnerabilities (7 SQL injections, 2 RCE, 1 auth bypass)
- 114 security tests covering all audit vectors
hmac.compare_digest()for all auth comparisons (timing-safe)- Nginx security headers — CSP, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy
- Rate limiting on
/api/filter/preview(30/min) and/api/filter/apply(20/min) pip-auditnow blocks CI on known CVEs (removed|| true)- Upload size validation — handles invalid env values, caps at 5GB
Architecture (Sprint S2)
- structlog migration — replaced
print()and stdlibloggingwith structlog in ESB workers and pg_notify - Silent exception logging — 6
except: passhandlers replaced withlog.debug()/log.warning() - Job cancellation race fix — check cancellation BEFORE persisting results
- Dataset load timeout — 300s max to prevent hangs on large files
- Trigger name collision fix — use trigger UUID as suffix (supports multiple triggers per table)
- WebSocket message limit — 1MB max per outgoing message
Observability (Sprint S4 + S6)
MetricsMiddleware— automatic HTTP metrics:gispulse_http_requests_total,gispulse_http_request_duration_seconds,gispulse_http_requests_in_flight- Path normalization — collapses UUIDs and numeric segments to reduce Prometheus cardinality
- Trace ID correlation —
trace_idin structured error logs for incident investigation - Docker non-root —
USER appuser(uid 1000) in Dockerfile .dockerignore— excludes .git, node_modules, tests, docs, .env, IDE files.pre-commit-config.yaml— ruff lint+format, trailing whitespace, YAML check, private key detection
Tests (Sprints S3 + S5)
- 2,439 tests passing (up from 2,205 in v1.0.1), +234 tests across 6 sprints
- 106 test files (unit + integration + security)
- Router coverage: 85% (23/27 routers tested, up from 33%)
- 16 new test files covering rules, triggers, jobs, datasets, CLI, persistence IO, auth, admin, scenarios, schedules, catalog, relations, filter, portal, ESB, tiles
- CI: mypy (type checking core modules) + ESLint/Vitest (frontend lint + tests)
[1.0.0] — 2026-04-06
Initial public release. 27 capabilities, 1,836 tests, multi-backend DuckDB/PostGIS engine.
[0.1.0] — 2026-03-31
Added
Core engine
- DuckDB geospatial engine with portable SpatiaLite and persistent PostGIS modes
SessionManagerwith E2E pipeline,ExecutionStrategypattern, SpatiaLite session supportJobRunnerwith async execution and job status tracking- Cross-layer operations: spatial join, reference layer system, multi-layer support
- Pagination, dataset association, project CRUD
- PyOGRIO migration for multi-format I/O
- Edge case hardening: shadow zones, centroid, area/length capabilities
- GeoParquet support and OGC server with MVT tile server
CLI
- Typer CLI entry point (
gispulse) - Commands:
init,validate,info,layers,formats,capabilities,serve,portal,doctor - Multi-format acceptance via the integrated I/O layer
Vector capabilities (10)
buffer,union,reproject,filter,clip,intersects,spatial_join,centroid,area_length,dissolve- Capability registry with auto-discovery
- Lifespan-managed capability injection
Rules
- Rules-as-config system with JSON definitions
- Rule editor UI with predicate builder
- Trigger-based rule evaluation with
auto_evaland SSE eval-stream
Persistence
- Persistent PostGIS mode with live sync and pg_notify integration
- Portable SpatiaLite mode (level 2 session, serverless)
- GPKG export from catalog
- Scene manager with snapshot and restore
REST API (FastAPI)
- Full REST API: projects, datasets, features, sessions, rules, triggers, scenarios
- 14 routers, 100+ endpoints
- Feature update, SQL execution, relation endpoints
- OGC Features ingestion endpoints
- SSE streaming for trigger evaluation results
- Docker hot-reload configuration for API and Portal dev servers
- Global error handlers
{"error": {"code", "message", "detail"}}for 400/404/422/500
Portal (React 19)
- 5-workspace layout: Explorer, Map, Workflows, Catalog, Data
- Layer tree with groups, color picker, legend and symbology
- Resizable panel layout with ActivityBar and Inspector
- Node editor (XyFlow/ReactFlow v12) with 9 node types, NodePalette, inline inspector
- Trigger stepper, scenario bar, spatial operations UI
- SQL console and feature inspector
- Catalog workspace with cards, favorites, mini-map, domain filtering
- Dark mode with OKLCH design tokens, Geist font, toast notifications
- Command palette (Ctrl+K), keyboard shortcuts (1–5, Ctrl+I/B/K/S/?)
- Drag-and-drop upload and URL import, GPKG export with QML styles
Viewer
- Embedded deck.gl spatial viewer served via
gispulse serve
ESB / Triggers
- Event bus with pg_notify, routing, circuit breaker, dead letter queue
- Trigger Builder UI with predicate composition
SessionProvisionerwithTriggerEvaluatorand SSE eval-stream
Catalog
- GIS data catalog: projections, basemaps, WMS/WFS feeds, open data sources
Tests
- 46 test files: unit and integration
- SpatiaLite E2E integration tests
- Pytest configuration with async support