From a68590ceec33516abf524b324f744bfaed10f439 Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 09:13:23 -0700 Subject: [PATCH 01/17] Fix SPM construction contracts and refresh current repository guidance --- GEO-INFER-AI/src/geo_infer_ai/utils/rng.py | 2 +- .../src/geo_infer_bayes/utils/rng.py | 2 +- GEO-INFER-COG/src/geo_infer_cog/utils/rng.py | 2 +- .../src/geo_infer_econ/utils/rng.py | 2 +- .../docs/geospatial/data_formats/h3/index.md | 9 +- .../getting_started/installation_guide.md | 10 +- .../spatial_analysis_basics.md | 3 +- .../docs/releases/0.4.0_migration.md | 22 +++ .../docs/releases/0.4.0_readiness.md | 27 ++- .../docs/support/troubleshooting.md | 34 ++-- .../src/geo_infer_math/utils/rng.py | 2 +- .../src/geo_infer_risk/utils/rng.py | 2 +- .../docs/deferred_statistical_methods.md | 3 +- .../src/geo_infer_spm/utils/helpers.py | 101 +++++++++-- GEO-INFER-SPM/src/geo_infer_spm/utils/rng.py | 2 +- GEO-INFER-SPM/tests/unit/AGENTS.md | 1 + GEO-INFER-SPM/tests/unit/README.md | 1 + .../tests/unit/test_helper_input_contracts.py | 163 ++++++++++++++++++ GEO-INFER-SPM/tests/unit/test_helpers.py | 19 +- README.md | 4 +- TODO.md | 163 ++++++------------ pyproject.toml | 2 +- 22 files changed, 391 insertions(+), 185 deletions(-) create mode 100644 GEO-INFER-SPM/tests/unit/test_helper_input_contracts.py diff --git a/GEO-INFER-AI/src/geo_infer_ai/utils/rng.py b/GEO-INFER-AI/src/geo_infer_ai/utils/rng.py index 63edc04f1..000cc6d78 100644 --- a/GEO-INFER-AI/src/geo_infer_ai/utils/rng.py +++ b/GEO-INFER-AI/src/geo_infer_ai/utils/rng.py @@ -63,7 +63,7 @@ def resolve_rng(seed: SeedLike = None) -> np.random.Generator: Raises ------ TypeError - If ``seed`` cannot produce a generator (including the legacy + If ``seed`` cannot produce a generator (including unsupported ``numpy.random.RandomState`` and the ``numpy.random`` module). Examples diff --git a/GEO-INFER-BAYES/src/geo_infer_bayes/utils/rng.py b/GEO-INFER-BAYES/src/geo_infer_bayes/utils/rng.py index 2c316c664..8cb420378 100644 --- a/GEO-INFER-BAYES/src/geo_infer_bayes/utils/rng.py +++ b/GEO-INFER-BAYES/src/geo_infer_bayes/utils/rng.py @@ -63,7 +63,7 @@ def resolve_rng(seed: SeedLike = None) -> np.random.Generator: Raises ------ TypeError - If ``seed`` cannot produce a generator (including the legacy + If ``seed`` cannot produce a generator (including unsupported ``numpy.random.RandomState`` and the ``numpy.random`` module). Examples diff --git a/GEO-INFER-COG/src/geo_infer_cog/utils/rng.py b/GEO-INFER-COG/src/geo_infer_cog/utils/rng.py index 9089b217b..9d5ebd632 100644 --- a/GEO-INFER-COG/src/geo_infer_cog/utils/rng.py +++ b/GEO-INFER-COG/src/geo_infer_cog/utils/rng.py @@ -63,7 +63,7 @@ def resolve_rng(seed: SeedLike = None) -> np.random.Generator: Raises ------ TypeError - If ``seed`` cannot produce a generator (including the legacy + If ``seed`` cannot produce a generator (including unsupported ``numpy.random.RandomState`` and the ``numpy.random`` module). Examples diff --git a/GEO-INFER-ECON/src/geo_infer_econ/utils/rng.py b/GEO-INFER-ECON/src/geo_infer_econ/utils/rng.py index 64a084b1a..fce2c2a8e 100644 --- a/GEO-INFER-ECON/src/geo_infer_econ/utils/rng.py +++ b/GEO-INFER-ECON/src/geo_infer_econ/utils/rng.py @@ -63,7 +63,7 @@ def resolve_rng(seed: SeedLike = None) -> np.random.Generator: Raises ------ TypeError - If ``seed`` cannot produce a generator (including the legacy + If ``seed`` cannot produce a generator (including unsupported ``numpy.random.RandomState`` and the ``numpy.random`` module). Examples diff --git a/GEO-INFER-INTRA/docs/geospatial/data_formats/h3/index.md b/GEO-INFER-INTRA/docs/geospatial/data_formats/h3/index.md index 6691d8f5f..8a8a923e3 100644 --- a/GEO-INFER-INTRA/docs/geospatial/data_formats/h3/index.md +++ b/GEO-INFER-INTRA/docs/geospatial/data_formats/h3/index.md @@ -63,14 +63,7 @@ use, and output schemas. construction. - Treat pentagons and antimeridian-crossing geometries as explicit edge cases. -## Common migration mistakes - -The following v3 names are not supported in GEO-INFER documentation or runtime -paths: - -```python -# Removed v3 names: geo_to_h3, h3_to_geo, h3_to_geo_boundary, k_ring -``` +## Verify the supported runtime Use the v4 table above. The repository H3 contract gate is: diff --git a/GEO-INFER-INTRA/docs/getting_started/installation_guide.md b/GEO-INFER-INTRA/docs/getting_started/installation_guide.md index 405e005a9..5045b88f4 100644 --- a/GEO-INFER-INTRA/docs/getting_started/installation_guide.md +++ b/GEO-INFER-INTRA/docs/getting_started/installation_guide.md @@ -76,13 +76,11 @@ For a complete pre-merge check, use the command list in the root - If an import resolves from an unexpected interpreter, use `uv run python` and inspect `python -c "import sys; print(sys.executable)"`. -## Do not use these legacy setup paths +## Environment ownership -- Do not install an unrelated PyPI package named `geo-infer` and assume it is - this checkout. -- Do not reintroduce `setup.py` or `requirements.txt` mirrors; modules build - from `pyproject.toml` and the root `uv.lock` pins resolution. -- Do not run examples with a system interpreter that bypasses the uv lockfile. +Install workspace packages from their `pyproject.toml` metadata and retain the +root `uv.lock`. Run examples through `uv run` from the checkout. A similarly named +package from a registry does not establish that this workspace is installed. ## Next step diff --git a/GEO-INFER-INTRA/docs/getting_started/spatial_analysis_basics.md b/GEO-INFER-INTRA/docs/getting_started/spatial_analysis_basics.md index 069e3504a..c1e69315e 100644 --- a/GEO-INFER-INTRA/docs/getting_started/spatial_analysis_basics.md +++ b/GEO-INFER-INTRA/docs/getting_started/spatial_analysis_basics.md @@ -103,8 +103,7 @@ print(f"Cell + neighbors: {len(neighbors)} cells") ``` **H3 v4 API note**: GEO-INFER uses the H3 v4 Python bindings. The function -names are `latlng_to_cell` and `cell_to_latlng` (not the legacy v3 names -`geo_to_h3` / `h3_to_geo`). All GEO-INFER documentation and code follows the +names are `latlng_to_cell` and `cell_to_latlng`. All GEO-INFER documentation and code follows the v4 convention. ## Core Spatial Operations diff --git a/GEO-INFER-INTRA/docs/releases/0.4.0_migration.md b/GEO-INFER-INTRA/docs/releases/0.4.0_migration.md index 6c2b51c4a..180164638 100644 --- a/GEO-INFER-INTRA/docs/releases/0.4.0_migration.md +++ b/GEO-INFER-INTRA/docs/releases/0.4.0_migration.md @@ -306,3 +306,25 @@ documented approximation. macOS 27 users affected by PyTensor's obsolete `-ld64` linker argument can select the explicit, guarded compiler profile in [native sampling profiles](../../../../GEO-INFER-SPM/docs/native_sampling_profiles.md). This profile runs the native compiler and preserves compiled sampling. + +## SPM construction helpers + +`generate_coordinates` requires a positive integer point count, four finite +ordered bounds, and a positive integer cluster count with a finite nonnegative +spread. Random grids use an explicit `Generator`; `None` draws fresh entropy. +Invalid cluster options fail before consuming a caller-owned random stream. + +`create_spatial_basis_functions` requires finite nonempty two-column coordinates +and a positive integer basis count. Polynomial columns follow increasing total +degree: intercept, latitude, longitude, latitude squared, latitude × longitude, +longitude squared, then cubic terms. The default ten-column polynomial basis now +terminates; the former per-axis exponent cap could never produce its tenth +column. Constant coordinate axes produce zero normalized terms; coincident +Gaussian centers produce finite unit responses. Zero-radius synthetic radial +signals also remain finite. + +The formula helper supports `~`, `+`, `*` as an explicit product, and `0` to +suppress the intercept. It rejects unknown product operands and empty designs. +It does not interpret `*` as automatic main-effect expansion or implement a +Patsy grammar. Explicit factor levels must be unique, nonempty, and include every +observed value; undeclared categories no longer silently become the reference. diff --git a/GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md b/GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md index 79363b669..9249f3b28 100644 --- a/GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md +++ b/GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md @@ -1,10 +1,27 @@ # GEO-INFER 0.4.0 candidate verification -This work prepares a main-branch candidate. It does not publish a tag or wheels. -The implementation preserves the legacy-sweep ancestry beginning at -`cbe6b9c59975254487452d697c2ea961a19f2286`; the hosted main baseline was -`510f1008e698b45aedc4306d65caf898da6cda04`. Changes are prepared on -`codex/0.4-readiness` before a non-forced main integration. +## Current verified checkpoint — 2026-10-08 + +The starting main commit for the modernization pass is +`6781c75ed56de99790a35bd815d7b39054c44444`. Local and remote main match, +and the preparation branch points to the same commit. All 21 jobs in +[main CI](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734618009) +passed, including both Python versions' test categories, installed wheels, +paired interchange, manuscript and validation. The separate +[Windows PLACE workflow](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734617885) +also passed. Generated signposts are current at this checkpoint. + +Main integration has occurred; the preparation-only description and previous +remote-main baseline are superseded. The implementation retains sweep ancestry +at `cbe6b9c59975254487452d697c2ea961a19f2286`. Historical failed attempts remain +available in immutable receipts and Git history. Open readiness issue bodies +still describe older checkpoints; their reconciliation is separate from the +observed published commit and successful workflow conclusions. + +The current pass adds SPM input and numerical contracts and replaces obsolete +operational guidance. It requires new local and hosted evidence after the changes. +Starting-main successes do not establish the new revision's acceptance, an +independent hosted-artifact audit, physical GPU validation or a package release. ## Stages and acceptance diff --git a/GEO-INFER-INTRA/docs/support/troubleshooting.md b/GEO-INFER-INTRA/docs/support/troubleshooting.md index 5d534261f..5e167f0ec 100644 --- a/GEO-INFER-INTRA/docs/support/troubleshooting.md +++ b/GEO-INFER-INTRA/docs/support/troubleshooting.md @@ -107,21 +107,25 @@ def reduce_memory(df): return df ``` -### H3 API Compatibility - -GEO-INFER-SPACE uses H3 v4. The v3 to v4 API changed function names. - -| v3 Function | v4 Function | -|-------------|-------------| -| `h3.geo_to_h3(lat, lng, res)` | `h3.latlng_to_cell(lat, lng, res)` | -| `h3.h3_to_geo(cell)` | `h3.cell_to_latlng(cell)` | -| `h3.h3_to_geo_boundary(cell)` | `h3.cell_to_boundary(cell)` | -| `h3.k_ring(cell, k)` | `h3.grid_disk(cell, k)` | -| `h3.h3_get_resolution(cell)` | `h3.get_resolution(cell)` | -| `h3.h3_to_parent(cell, res)` | `h3.cell_to_parent(cell, res)` | -| `h3.h3_to_children(cell, res)` | `h3.cell_to_children(cell, res)` | - -If you see `AttributeError: module 'h3' has no attribute 'geo_to_h3'`, you have H3 v4 installed but are calling v3 functions. +### H3 runtime contract + +GEO-INFER-SPACE uses the locked H3 4.x runtime. Use these supported operations: + +| Operation | Function | +|-----------|----------| +| Latitude/longitude to cell | `h3.latlng_to_cell(lat, lng, resolution)` | +| Cell centroid | `h3.cell_to_latlng(cell)` | +| Cell boundary | `h3.cell_to_boundary(cell)` | +| Neighborhood | `h3.grid_disk(cell, radius)` | +| Resolution | `h3.get_resolution(cell)` | +| Parent | `h3.cell_to_parent(cell, resolution)` | +| Children | `h3.cell_to_children(cell, resolution)` | + +For a missing H3 attribute, verify the installed package with `uv pip show h3`, +restore the locked environment, and check the supported operation above. +The repository wrappers enforce identity, resolution and allocation contracts; +run `uv run python GEO-INFER-TEST/run_unified_tests.py --h3-migration` to verify +composition with ACT. ### Dependency Conflicts diff --git a/GEO-INFER-MATH/src/geo_infer_math/utils/rng.py b/GEO-INFER-MATH/src/geo_infer_math/utils/rng.py index fa15e3cbc..573c86681 100644 --- a/GEO-INFER-MATH/src/geo_infer_math/utils/rng.py +++ b/GEO-INFER-MATH/src/geo_infer_math/utils/rng.py @@ -62,7 +62,7 @@ def resolve_rng(seed: SeedLike = None) -> np.random.Generator: Raises ------ TypeError - If ``seed`` cannot produce a generator (including the legacy + If ``seed`` cannot produce a generator (including unsupported ``numpy.random.RandomState`` and the ``numpy.random`` module). Examples diff --git a/GEO-INFER-RISK/src/geo_infer_risk/utils/rng.py b/GEO-INFER-RISK/src/geo_infer_risk/utils/rng.py index 43d27ccdc..80e1c4a2c 100644 --- a/GEO-INFER-RISK/src/geo_infer_risk/utils/rng.py +++ b/GEO-INFER-RISK/src/geo_infer_risk/utils/rng.py @@ -63,7 +63,7 @@ def resolve_rng(seed: SeedLike = None) -> np.random.Generator: Raises ------ TypeError - If ``seed`` cannot produce a generator (including the legacy + If ``seed`` cannot produce a generator (including unsupported ``numpy.random.RandomState`` and the ``numpy.random`` module). Examples diff --git a/GEO-INFER-SPM/docs/deferred_statistical_methods.md b/GEO-INFER-SPM/docs/deferred_statistical_methods.md index 59a164ead..349511849 100644 --- a/GEO-INFER-SPM/docs/deferred_statistical_methods.md +++ b/GEO-INFER-SPM/docs/deferred_statistical_methods.md @@ -119,7 +119,8 @@ of carrying unexplained deferral text. ### Formula parser - **Site:** `utils/helpers.py` `_parse_formula` - **Ships today:** `~` / `+` / `*` grammar over covariates, intercept - suppression via `0`. + suppression via `0`. Unknown interaction operands, empty terms and empty + designs are rejected; factor values must belong to nonempty unique levels. - **Full estimator requires:** patsy-style grammar: `-` term removal, `:` pure interactions, `I()` transforms, polynomial/spline term functions, and factor-level encoding inside formulas. diff --git a/GEO-INFER-SPM/src/geo_infer_spm/utils/helpers.py b/GEO-INFER-SPM/src/geo_infer_spm/utils/helpers.py index e9566fc52..5acb366f9 100644 --- a/GEO-INFER-SPM/src/geo_infer_spm/utils/helpers.py +++ b/GEO-INFER-SPM/src/geo_infer_spm/utils/helpers.py @@ -6,6 +6,7 @@ """ from typing import Any +from numbers import Real import numpy as np from scipy import stats @@ -92,10 +93,12 @@ def _parse_formula( ) -> tuple[np.ndarray, list[str]]: """Parse formula string to create design matrix (see register: "Formula parser").""" # Deferred: see docs/deferred_statistical_methods.md ("Formula parser"). - if "~" not in formula: - raise ValueError("Formula must contain '~' separator") + if formula.count("~") != 1: + raise ValueError("Formula must contain exactly one '~' separator") response, predictors = formula.split("~", 1) + if not response.strip() or not predictors.strip(): + raise ValueError("Formula needs a response and predictor terms") # Parse predictors terms = [term.strip() for term in predictors.split("+")] @@ -120,13 +123,16 @@ def _parse_formula( # Interaction term (see register: "Formula parser"). var1, var2 = term.split("*", 1) var1, var2 = var1.strip(), var2.strip() - if var1 in covariates_map and var2 in covariates_map: - interaction = covariates_map[var1] * covariates_map[var2] - design_components.append(interaction) - names.append(f"{var1}:{var2}") + if var1 not in covariates_map or var2 not in covariates_map: + raise ValueError(f"Unknown interaction in formula: {term}") + interaction = covariates_map[var1] * covariates_map[var2] + design_components.append(interaction) + names.append(f"{var1}:{var2}") else: raise ValueError(f"Unknown term in formula: {term}") + if not design_components: + raise ValueError("Formula must select at least one regressor") return np.column_stack(design_components), names @@ -134,6 +140,12 @@ def _create_dummy_variables(values: np.ndarray, levels: list[str]) -> np.ndarray """Create dummy variables for categorical factor.""" n_points = len(values) n_levels = len(levels) + if not levels or len(set(levels)) != n_levels: + raise ValueError("Factor levels must be nonempty and unique") + if np.asarray(values).ndim != 1: + raise ValueError("Factor values must be one-dimensional") + if any(value not in levels for value in values): + raise ValueError("Factor values must belong to the declared levels") # Map string levels to indices level_to_idx = {level: i for i, level in enumerate(levels)} @@ -164,9 +176,8 @@ def generate_coordinates( grid_type: Type of coordinate grid ('regular', 'random', 'clustered') n_points: Number of coordinate points to generate bounds: Spatial bounds (min_lon, max_lon, min_lat, max_lat) - random_seed: Optional seed for reproducible random/clustered grids. - When ``None`` (default) the legacy global ``np.random`` state is - used, preserving existing behaviour. + random_seed: Seed or generator for reproducible random/clustered grids. + ``None`` draws fresh OS entropy without changing global random state. **kwargs: Additional parameters for grid generation Returns: @@ -182,12 +193,39 @@ def generate_coordinates( >>> # Reproducible random coordinates >>> coords = generate_coordinates('random', n_points=50, random_seed=7) """ - rng = resolve_rng(random_seed) - + if ( + isinstance(n_points, (bool, np.bool_)) + or not isinstance(n_points, (int, np.integer)) + or n_points <= 0 + ): + raise ValueError("n_points must be a positive integer") + if grid_type not in {"regular", "random", "clustered"}: + raise ValueError(f"Unknown grid type: {grid_type}") if bounds is None: bounds = (-180, 180, -90, 90) # Global bounds - min_lon, max_lon, min_lat, max_lat = bounds + bounds_array = np.asarray(bounds, dtype=float) + if bounds_array.shape != (4,) or not np.isfinite(bounds_array).all(): + raise ValueError("bounds must contain four finite values") + min_lon, max_lon, min_lat, max_lat = bounds_array + if min_lon > max_lon or min_lat > max_lat: + raise ValueError("bounds minima must not exceed maxima") + if grid_type == "clustered": + n_clusters = kwargs.get("n_clusters", 3) + cluster_std = kwargs.get("cluster_std", 5.0) + if ( + isinstance(n_clusters, (bool, np.bool_)) + or not isinstance(n_clusters, (int, np.integer)) + or n_clusters <= 0 + ): + raise ValueError("n_clusters must be a positive integer") + if ( + not isinstance(cluster_std, Real) + or not np.isfinite(cluster_std) + or cluster_std < 0 + ): + raise ValueError("cluster_std must be finite and nonnegative") + rng = resolve_rng(random_seed) if grid_type == "regular": # Create regular grid @@ -275,6 +313,14 @@ def generate_synthetic_data( >>> data = generate_synthetic_data(coords, effects={'trend': 'north_south'}) """ rng = resolve_rng(random_seed) + coordinates = np.asarray(coordinates, dtype=float) + if ( + coordinates.ndim != 2 + or coordinates.shape[1] != 2 + or not len(coordinates) + or not np.isfinite(coordinates).all() + ): + raise ValueError("coordinates must be a nonempty finite (n_points, 2) array") n_points = len(coordinates) if effects is None: @@ -307,7 +353,7 @@ def generate_synthetic_data( # Radial pattern from center center = np.mean(coordinates, axis=0) distances = np.linalg.norm(coordinates - center, axis=1) - dist_norm = distances / np.max(distances) + dist_norm = distances / (np.max(distances) or 1.0) signal += 5 * (1 - dist_norm) # Higher values near center # Spatial clusters @@ -395,7 +441,9 @@ def create_spatial_basis_functions( Args: coordinates: Spatial coordinates (n_points, 2) n_basis: Number of basis functions - method: Basis function method ('gaussian', 'polynomial', 'fourier') + method: Basis function method ('gaussian', 'polynomial', 'fourier'). + Polynomial columns follow increasing total degree, starting with + the intercept, latitude, longitude, then quadratic terms. random_seed: Optional seed for reproducible Gaussian center selection. When ``None``, center selection uses a fresh entropy-backed generator without mutating the global ``np.random`` state. @@ -403,6 +451,20 @@ def create_spatial_basis_functions( Returns: Basis function matrix (n_points, n_basis) """ + coordinates = np.asarray(coordinates, dtype=float) + if ( + coordinates.ndim != 2 + or coordinates.shape[1] != 2 + or not len(coordinates) + or not np.isfinite(coordinates).all() + ): + raise ValueError("coordinates must be a nonempty finite (n_points, 2) array") + if ( + isinstance(n_basis, (bool, np.bool_)) + or not isinstance(n_basis, (int, np.integer)) + or n_basis <= 0 + ): + raise ValueError("n_basis must be a positive integer") n_points = len(coordinates) if method == "gaussian": @@ -418,7 +480,7 @@ def create_spatial_basis_functions( coordinates[:, np.newaxis] - centers[np.newaxis, :], axis=2 ) median_dist = np.median(distances) - width = median_dist / np.sqrt(n_basis) + width = (median_dist or 1.0) / np.sqrt(n_basis) basis = np.zeros((n_points, n_basis)) for i in range(n_basis): @@ -432,8 +494,8 @@ def create_spatial_basis_functions( lon, lat = coordinates[:, 0], coordinates[:, 1] # Normalize coordinates - lon_norm = (lon - np.mean(lon)) / np.std(lon) - lat_norm = (lat - np.mean(lat)) / np.std(lat) + lon_norm = (lon - np.mean(lon)) / (np.std(lon) or 1.0) + lat_norm = (lat - np.mean(lat)) / (np.std(lat) or 1.0) basis_list = [np.ones(n_points)] # Constant @@ -441,8 +503,9 @@ def create_spatial_basis_functions( while len(basis_list) < n_basis: for i in range(degree + 1): j = degree - i - if i <= 2 and j <= 2: # Limit to degree 2 to avoid overfitting - basis_list.append(lon_norm**i * lat_norm**j) + basis_list.append(lon_norm**i * lat_norm**j) + if len(basis_list) == n_basis: + break degree += 1 diff --git a/GEO-INFER-SPM/src/geo_infer_spm/utils/rng.py b/GEO-INFER-SPM/src/geo_infer_spm/utils/rng.py index 13da98d0f..7766c70c0 100644 --- a/GEO-INFER-SPM/src/geo_infer_spm/utils/rng.py +++ b/GEO-INFER-SPM/src/geo_infer_spm/utils/rng.py @@ -62,7 +62,7 @@ def resolve_rng(seed: SeedLike = None) -> np.random.Generator: Raises ------ TypeError - If ``seed`` cannot produce a generator (including the legacy + If ``seed`` cannot produce a generator (including unsupported ``numpy.random.RandomState`` and the ``numpy.random`` module). Examples diff --git a/GEO-INFER-SPM/tests/unit/AGENTS.md b/GEO-INFER-SPM/tests/unit/AGENTS.md index 0e851701e..5b23287df 100644 --- a/GEO-INFER-SPM/tests/unit/AGENTS.md +++ b/GEO-INFER-SPM/tests/unit/AGENTS.md @@ -29,6 +29,7 @@ - `test_contrasts.py` - `test_data_io.py` - `test_glm.py` +- `test_helper_input_contracts.py` - `test_helpers.py` - `test_helpers_reproducibility.py` - `test_package_exports.py` diff --git a/GEO-INFER-SPM/tests/unit/README.md b/GEO-INFER-SPM/tests/unit/README.md index 435b11bd4..e12ec533f 100644 --- a/GEO-INFER-SPM/tests/unit/README.md +++ b/GEO-INFER-SPM/tests/unit/README.md @@ -12,6 +12,7 @@ Unit workspace within `GEO-INFER-SPM`. - `test_contrasts.py` - `test_data_io.py` - `test_glm.py` +- `test_helper_input_contracts.py` - `test_helpers.py` - `test_helpers_reproducibility.py` - `test_package_exports.py` diff --git a/GEO-INFER-SPM/tests/unit/test_helper_input_contracts.py b/GEO-INFER-SPM/tests/unit/test_helper_input_contracts.py new file mode 100644 index 000000000..6bcb0d14e --- /dev/null +++ b/GEO-INFER-SPM/tests/unit/test_helper_input_contracts.py @@ -0,0 +1,163 @@ +"""Input rejection and numerical oracles for public SPM construction helpers.""" + +import subprocess +import sys + +import numpy as np +import pytest + +from geo_infer_spm.models.data_models import SPMData +from geo_infer_spm.utils.helpers import ( + create_design_matrix, + create_spatial_basis_functions, + generate_coordinates, + generate_synthetic_data, +) + + +@pytest.mark.parametrize("count", [0, -1, 1.5, True, np.bool_(False)]) +@pytest.mark.parametrize("grid", ["regular", "random", "clustered"]) +def test_coordinate_count_is_a_positive_integer(count, grid): + with pytest.raises(ValueError, match="n_points"): + generate_coordinates(grid, n_points=count) + + +@pytest.mark.parametrize( + "bounds", + [(2, 1, 0, 1), (0, 1, 2, 1), (0, 1, 2), (0, np.inf, 0, 1), (0, 1, np.nan, 1)], +) +def test_coordinate_bounds_are_finite_and_ordered(bounds): + with pytest.raises(ValueError, match="bounds"): + generate_coordinates(bounds=bounds) + + +@pytest.mark.parametrize( + "options", + [ + {"n_clusters": 0}, + {"n_clusters": 1.5}, + {"n_clusters": True}, + {"cluster_std": -1}, + {"cluster_std": np.nan}, + {"cluster_std": np.inf}, + {"cluster_std": "wide"}, + ], +) +def test_cluster_options_reject_invalid_inputs_without_consuming_rng(options): + rng = np.random.default_rng(17) + control = np.random.default_rng(17) + with pytest.raises(ValueError, match="n_clusters|cluster_std"): + generate_coordinates("clustered", random_seed=rng, **options) + assert rng.integers(2**32) == control.integers(2**32) + + +@pytest.mark.parametrize("method", ["gaussian", "polynomial", "fourier"]) +def test_colocated_basis_is_finite(method): + with np.errstate(all="raise"): + basis = create_spatial_basis_functions( + np.zeros((4, 2)), n_basis=6, method=method, random_seed=3 + ) + assert basis.shape == (4, 6) + assert np.isfinite(basis).all() + if method == "gaussian": + np.testing.assert_array_equal(basis, np.ones((4, 6))) + elif method == "polynomial": + np.testing.assert_array_equal(basis[:, 0], np.ones(4)) + np.testing.assert_array_equal(basis[:, 1:], np.zeros((4, 5))) + + +@pytest.mark.parametrize("count", [0, -1, 2.5, True]) +def test_basis_count_is_positive_integer(count): + with pytest.raises(ValueError, match="n_basis"): + create_spatial_basis_functions(np.zeros((4, 2)), n_basis=count) + + +def test_polynomial_default_terminates_and_matches_total_degree_oracle(): + # A bounded real child protects the suite against the former infinite loop + # for the public default n_basis=10. + code = """ +import numpy as np +from geo_infer_spm.utils.helpers import create_spatial_basis_functions +coords = np.array([[-1., -2.], [0., 0.], [1., 2.]]) +x, y = ((coords - coords.mean(axis=0)) / coords.std(axis=0)).T +expected = np.column_stack([np.ones(3), y, x, y*y, x*y, x*x, y**3, x*y*y, x*x*y, x**3]) +actual = create_spatial_basis_functions(coords, method="polynomial") +np.testing.assert_allclose(actual, expected) +""" + subprocess.run( + [sys.executable, "-c", code], + check=True, + timeout=20, + capture_output=True, + text=True, + ) + + +@pytest.mark.parametrize( + "coords", [np.empty((0, 2)), np.zeros(3), np.zeros((3, 3)), np.array([[np.nan, 0]])] +) +@pytest.mark.parametrize( + "helper", [create_spatial_basis_functions, generate_synthetic_data] +) +def test_coordinate_consumers_reject_invalid_shape_and_nonfinite_values(coords, helper): + with pytest.raises(ValueError, match="coordinates"): + helper(coords) + + +def test_radial_signal_at_coincident_coordinates_is_finite(): + with np.errstate(all="raise"): + data = generate_synthetic_data( + np.zeros((4, 2)), effects={"trend": "radial"}, noise_level=0, random_seed=1 + ) + np.testing.assert_array_equal(data.data, np.full(4, 5.0)) + + +def _data(): + return SPMData( + data=np.arange(3.0), + coordinates=np.zeros((3, 2)), + covariates={ + "x": np.arange(3.0), + "y": np.arange(3.0) + 2, + "kind": np.array(["a", "b", "a"]), + }, + ) + + +@pytest.mark.parametrize( + "formula", + [ + "y ~ x * missing", + "y ~ missing * x", + "y ~ 0", + "y ~", + "~ x", + "y ~ x ~ y", + "y ~ x +", + ], +) +def test_formula_rejects_silent_omission_and_empty_design(formula): + with pytest.raises(ValueError): + create_design_matrix(_data(), formula=formula) + + +def test_supported_formula_interaction_matches_exact_values(): + data = _data() + design = create_design_matrix(data, formula="response ~ x + x * y") + assert design.names == ["intercept", "x", "x:y"] + np.testing.assert_array_equal( + design.matrix, + np.column_stack( + [ + np.ones(3), + data.covariates["x"], + data.covariates["x"] * data.covariates["y"], + ] + ), + ) + + +@pytest.mark.parametrize("levels", [[], ["a", "a"], ["a", "c"]]) +def test_factor_encoding_rejects_ambiguous_levels(levels): + with pytest.raises(ValueError, match="Factor"): + create_design_matrix(_data(), factors={"kind": levels}) diff --git a/GEO-INFER-SPM/tests/unit/test_helpers.py b/GEO-INFER-SPM/tests/unit/test_helpers.py index 517a1c35e..7f870f031 100644 --- a/GEO-INFER-SPM/tests/unit/test_helpers.py +++ b/GEO-INFER-SPM/tests/unit/test_helpers.py @@ -99,11 +99,20 @@ def test_invalid_covariate_name(self): def test_formula_based_design(self): """Test formula-based design matrix creation.""" - # This would require implementing formula parsing - # For now, test basic functionality - design = create_design_matrix(self.spm_data, covariates=["elevation"]) - - assert design is not None + design = create_design_matrix( + self.spm_data, formula="response ~ elevation + temperature" + ) + assert design.names == ["intercept", "elevation", "temperature"] + np.testing.assert_array_equal( + design.matrix, + np.column_stack( + [ + np.ones(50), + self.covariates["elevation"], + self.covariates["temperature"], + ] + ), + ) class TestCoordinateGeneration: diff --git a/README.md b/README.md index 314d17573..4a0015931 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ domain modeling, agent workflows, and reproducible repository validation in one | --- | ---: | | Modules | 45 | | Python source files | 994 | -| Python test files | 785 | +| Python test files | 786 | | Repository README.md files | 807 | | Repository AGENTS.md files | 804 | @@ -138,7 +138,7 @@ the exact reproducible exception list. | `GEO-INFER-SEC` | `geo_infer_sec` | 22 | 20 | | `GEO-INFER-SIM` | `geo_infer_sim` | 11 | 9 | | `GEO-INFER-SPACE` | `geo_infer_space` | 84 | 50 | -| `GEO-INFER-SPM` | `geo_infer_spm` | 27 | 22 | +| `GEO-INFER-SPM` | `geo_infer_spm` | 27 | 23 | | `GEO-INFER-TEST` | `geo_infer_test` | 22 | 52 | | `GEO-INFER-TIME` | `geo_infer_time` | 20 | 23 | | `GEO-INFER-TRANSPORT` | `geo_infer_transport` | 7 | 10 | diff --git a/TODO.md b/TODO.md index 96ff3c86c..c6bc48fe9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # GEO-INFER Open Task & Backlog Ledger -> Last reviewed: 2026-10-05 +> Last reviewed: 2026-10-08 > Scope: Multi-package repository (`GEO-INFER`) across workspace packages and 45 domain modules. > Centralization Rule: All planned, open, or deferred engineering work across all modules is tracked exclusively in this ledger. Module source code and tests must never carry local task markers (`TODO`, `FIXME`, `XXX`, `HACK`). > History note (2026-09-07): the published history was rewritten to re-attribute @@ -26,111 +26,46 @@ audit: the previously open rows verified current against the tree, delivered claims were spot-checked, and module/package surfaces were swept for untracked next steps. -### 0.4.0 continuation — current scope (2026-10-05) - -**Execution resumed:** on 2026-10-05 the owner authorized comprehensive issue -fixes, verification and an ordinary main push. Candidate acceptance remains -required before integration. Tagging, release attachments, registry publication -and deployment retain their separate authorization boundary; accounts, -credentials, configured models and hooks remain under human control. - -At the resumed verification checkpoint, preparation branch `codex/0.4-readiness` -was pushed at `a254a2856e4c1489ee31ea6c0183542d5db18967`; remote `main` remains -`510f1008e698b45aedc4306d65caf898da6cda04`. The local legacy sweep ancestor -`cbe6b9c59975254487452d697c2ea961a19f2286` must be preserved. **Readiness is -NO GO.** The last candidate's hosted TEST coverage was 72.22955% against the -unchanged 85% floor; other successes remain bound to their original revision. - -At implementation resume there were 39 pending implementation paths: root and 36 member coverage -configurations, the TEST tracing regression, and the owning testing guide. -Their correction removes only the erroneous `*/test_*` source omission. -The 37-configuration × three-filename matrix passed 111 cases in each of two -narrow recording profiles. The original narrow inner SQLite traces were not -retained; the later whole TEST diagnostic retained all 111 fresh traces. - -The whole diagnostic **completed naturally with FAIL**, before a requested stop -could be sent: **1,815 passed / one failed / 1,816 executed**. The ordinary -validator control -`test_validator_arguments_do_not_advertise_pytest_or_report_promises[arguments1]` -recorded a real TIMEOUT (5-second envelope, 5.799 seconds elapsed, return code -`None`) despite emitting its expected output. This establishes a timeout to -investigate, not a classifier defect or an environmental cause. Independently, -raw coverage was **2,529/3,032 = 83.41029%**, still below 85%. The attempt is -diagnostic evidence, not accepted TEST/floor evidence. No signal was issued; -the observed owned processes were absent after completion. Original manuscript -output remains an older failed-candidate bundle. - -Continuation repairs now separate census failures from pipe polling, enforce the -original deadline around final ownership inspection, retain admitted parallel -worker outcomes through repeated shutdown interruption, and reject empty runner -selections. Focused regressions establish these defects independently of the -historical timeout, whose cause remains unresolved. The manuscript custody path -now rejects root/directory/file replacement during validation; its focused -independent review accepted the declared boundary. Native Windows PLACE workflow -implementation is present, with hosted runtime acceptance still pending. These -are implementation and narrow-test results; whole-package coverage and final -candidate acceptance remain required. - -The resumed clean-candidate whole TEST attempt reached its 1,800-second deadline -without final JUnit or raw coverage output. Source stayed fixed, all 111 policy -traces were retained, and its owned processes exited. This is failed acceptance; -the progress stream contains failures whose complete dispositions require fresh -diagnosis. Aggregate native census time was 1,517 seconds over 1,107 calls. -A synthetic parser assessment demonstrated avoidable token-negative regex work; -the resulting prefilter passed 53 focused ownership tests and independent -review; renewed whole-package acceptance is still required. These observations -do not establish the historical ordinary-validator timeout's cause. - -Both initial native Windows PLACE legs failed before collection because the -canonical runner injected uninstalled optional DATA source into the minimal -PLACE profile, exposing its absent SQLAlchemy dependency. The corrected workflow -uses the existing execution API with observer-only `PYTHONPATH`, a separate real -poisoning negative control and both complete acceptance files. Its 22 structural -and custody tests passed. The separate locked macOS Python 3.12 profile reproduced -the negative failure and passed all 51 positive cases plus custody. Independent -source review accepted both scoped repairs; native runtime proof remains pending. -Hosted artifact reconciliation also identified Windows LF-to-CRLF lock conversion; -an explicit LF attribute and real Git checkout regression preserve raw lock -custody. The updated workflow/metadata suite passed 68 tests. Fresh native receipt -hashes must still be checked on the renewed candidate. - -The next whole TEST attempt completed all 1,953 selected cases with source -unchanged and all 111 policy traces retained: 1,951 passed and two deadline/census -controls failed. Raw coverage was 2,921/3,325 = 87.84962%, above the unchanged -85% floor, but failed tests still reject the measurement. The controls assumed -the child exited before its first 250 ms pipe poll. Their correction observes -actual target completion, distinguishes preliminary/final/cleanup scans and -forces a real incomplete poll; production deadlines remain unchanged. All four -focused cases passed canonical receipt acceptance. Fresh independent review -accepted the repair and confirmed that a retry mutation fails both parameter -cases. Renewed whole TEST proof remains required. - -The next native Windows PLACE legs reached all 51 cases on Python 3.11/3.12: -48 passed and three failed on converted fixture bytes or default text decoding. -Both sealed artifacts reconciled with no hash errors and the exact raw lock; -all ten observed workers per leg were reaped with closed streams. Exact archived -source attributes and LF replay attributes preserve the existing hash contracts; -the renderer test now reads UTF-8 explicitly and exercises a cp1252 default. -Local testcases passed, but canonical acceptance rejected an owned-process leak -in the new temporary Git checkout regression. A focused diagnostic identified -the inherited Git fsmonitor setting: the test started an owned background daemon. -Command-local fsmonitor disabling in this temporary checkout preserved real -autocrlf conversion and passed canonical acceptance with no owned processes or -daemon artifacts. Final local locked-profile acceptance passed all 52 cases -with sealed receipt hashes and all ten observed workers reaped with closed -streams. Fresh final independent review accepted the scoped repair; renewed -native Windows acceptance remains required. - -On clean candidate `69ed0121601f026bc16debbe736d657199468de9`, whole TEST -passed all 1,954 cases with 2,922/3,325 = 87.879699% raw coverage, all 111 policy -traces, historical inventory retention and unchanged source. Both native Windows -PLACE legs also passed. Their artifact reconciliation remains separate. Full CI -exposed one forced-poll process-control assertion requiring a remaining budget -strictly below the original budget; the preliminary scan can occur before the -monotonic clock advances. Its test-only correction permits equality while still -rejecting budget extension. All four focused cases passed canonical acceptance. -Fresh independent review accepted the scoped bound and confirmed prior retry -mutation evidence still applies. Renewed exact-candidate acceptance remains required. +### 0.4.0 continuation — current scope (2026-10-08) + +The owner authorized repo-wide modernization, verification and an ordinary main +push on 2026-10-08. Release tags, registry publication, deployment, licensed data, +external hardware and account changes remain separately scoped. + +The verified starting checkout is clean at +`6781c75ed56de99790a35bd815d7b39054c44444`, matching remote `main` and the +preparation branch. Main integration has occurred. The earlier statement that +main remained at `510f1008e698b45aedc4306d65caf898da6cda04` is superseded. +The preserved sweep ancestor is `cbe6b9c59975254487452d697c2ea961a19f2286`. +Historical failed attempts remain in immutable receipt directories and Git +history; they are not current acceptance verdicts. + +At this exact starting revision, [main CI](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734618009) +completed all 21 jobs successfully, including Python 3.11/3.12 unit, slow, +integration, system, performance and H3 categories, installed wheels, paired +interchange, manuscript and validation. [Native Windows PLACE](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734617885), +[portability](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37736394591), +[formatting](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37736398107) +and [standalone interchange](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37736401172) +also passed. These are live workflow conclusions, not a fresh independent audit +of every uploaded artifact. The issue tracker has not yet been reconciled with +these results: required issues #61–68 remain open. Their original bodies describe +older checkpoints and must not override live Git and workflow evidence. + +The modernization pass replaces obsolete H3 setup guidance with supported APIs, +corrects RNG documentation, and repairs SPM helper input rejection, silently +omitted formula interactions, nonfinite degenerate bases and the default +polynomial basis's nonterminating expansion. Migration guidance records the +new polynomial ordering and explicit supported formula grammar. Working public +API names, security rejection tests, publisher URLs and hash-bound historical +identities remain meaningful contracts rather than removal targets. + +Final acceptance for this pass requires focused SPM numerical/rejection tests, +generated-signpost review, every strict repository gate and broad module +categories. New results remain bound to their measured source; the starting +revision's green hosted results cannot be relabeled as new-revision acceptance. +See [current candidate verification](GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md) +for the retained evidence boundaries and next acceptance steps. The [readiness tracker (#61)](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/61) links the complete next-work graph. Dependencies govern completion; preparation @@ -140,17 +75,17 @@ below; their historical failed evidence remains unchanged. | ID | Owning area / status | Complete bounded next step | Acceptance / dependencies | | --- | --- | --- | --- | -| **R04-00** | Repository / NO GO tracker | [#61: exact-candidate readiness](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/61) coordinates every required child and separate deferred scope. | One clean revision binds local, hosted, wheel, paired, security and manuscript evidence; successful required children precede main integration. | +| **R04-00** | Repository / acceptance reconciliation open | [#61: exact-candidate readiness](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/61) coordinates every required child and separate deferred scope. | One clean revision binds local, hosted, wheel, paired, security and manuscript evidence; successful required children precede main integration. | | **R04-01** | TEST / demonstrated timeout, cause unresolved | [#62: ordinary-validator timeout](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/62): inspect actual child/parent timing, ownership census, cleanup and receipts; repair only a demonstrated contract problem. | Real command outcomes, fresh nonzero JUnit where promised, validators without JUnit, invalid/empty selections, POSIX/Windows descendants and interruption; no automatic retries, suppressed errors or production-budget workaround. Fresh independent shared-infrastructure review. | -| **R04-02** | TEST / source omission fixed locally; coverage below floor | [#63: real runner behavior and 85% floor](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/63): review policy fix; add meaningful public runner/error/cancellation behavior coverage. | Preserve the 116 existing core behavior identities and 111 policy controls; fully passing whole TEST measurement with genuine raw coverage ≥85%. Complete all 46 final package/ROOT dispositions and inventory reconciliation. Depends on R04-01. | -| **R04-03** | Repository, SPACE/TIME/ACT and connected modules / final local proof pending | [#64: strict and analytical acceptance](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/64): freeze reviewed source and complete all declared local contracts/categories, real composition oracles, security and reproducibility; reduce measured fixture/setup cost without losing coverage. | UTC/DST/ns/step/gap/order/ownership/allocation contracts; real DATA→SPACE→TIME, BAYES/ACT permutations, seven-cell/pentagon transitions/posteriors/policy, IOT coordinate oracle, ART hostile input, OPS port cleanup and async lifecycle. Strict repository/package/docs/skills/test/model/logging/lint/format/workflow/secret gates. Depends on R04-01/R04-02. | -| **R04-04** | Packaging and GNN / fresh exact-candidate proof pending | [#65: all wheels, optional boundaries and paired interchange](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/65). | 45 wheels, 45 base and 20 operation profiles outside checkout on 3.11/3.12; metadata/resources/extra parity, real signed DuckDB fast path; exact pinned GNN four-family/exporter/replay evidence. Stable candidate from R04-03. | -| **R04-05 / CODE-01** | Documentation, manuscript, index / convergence and full proof pending | [#66: executable docs and manuscript](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/66): finish truthful API/migration/configuration guidance, generated signposts, 0.4.0 parity, Git-derived metadata fixed point and source-current GitNexus. | Coordinator-only full eleven-group producer, independent whole-output custody/snapshot, real native PDF/figure inspection, generator drift checks and fresh ROOT inventory (currently 142+7). Retain producer manifest/inventory/variables/figure-registry JSON or state hosted upload limits. No old-output or default-seven-tier passing claim. Depends on stable R04-03 source. | -| **R04-06** | CI / fresh corrected-candidate evidence pending | [#67: complete hosted evidence](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/67): run declared workflows once on the reviewed candidate and audit actual receipts/artifact bytes. | Required unit/integration/performance/system/slow/H3 matrix on 3.11/3.12, native Windows and early contracts; actual profile/inventory reconciliation, all 46 floors, wheels/paired/ROOT/models/security and complete uploaded artifacts. Derive fresh counts; preserve failures. Depends on R04-01 through R04-05. | -| **R04-07** | Git/main / held, not integrated | [#68: accepted main integration](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/68): when implementation is resumed and candidate GO exists, recheck remote/protection/ancestry, integrate ordinarily and verify exact published/main-triggered evidence. | No force-push, ancestry rewrite, stale-SHA acceptance or protection bypass; preserve rollback and unrelated work. Main SHA parity plus actual same-SHA hosted results. Depends on R04-03 through R04-06. | +| **R04-02** | TEST / hosted coverage gate passed; final receipt reconciliation open | [#63: real runner behavior and 85% floor](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/63): review policy fix; add meaningful public runner/error/cancellation behavior coverage. | Preserve the 116 existing core behavior identities and 111 policy controls; fully passing whole TEST measurement with genuine raw coverage ≥85%. Complete all 46 final package/ROOT dispositions and inventory reconciliation. Depends on R04-01. | +| **R04-03** | Repository and connected modules / fresh modernization proof required | [#64: strict and analytical acceptance](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/64): freeze reviewed source and complete all declared local contracts/categories, real composition oracles, security and reproducibility; reduce measured fixture/setup cost without losing coverage. | UTC/DST/ns/step/gap/order/ownership/allocation contracts; real DATA→SPACE→TIME, BAYES/ACT permutations, seven-cell/pentagon transitions/posteriors/policy, IOT coordinate oracle, ART hostile input, OPS port cleanup and async lifecycle. Strict repository/package/docs/skills/test/model/logging/lint/format/workflow/secret gates. Depends on R04-01/R04-02. | +| **R04-04** | Packaging and GNN / starting-main hosted profiles passed | [#65: all wheels, optional boundaries and paired interchange](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/65). | 45 wheels, 45 base and 20 operation profiles outside checkout on 3.11/3.12; metadata/resources/extra parity, real signed DuckDB fast path; exact pinned GNN four-family/exporter/replay evidence. Stable candidate from R04-03. | +| **R04-05 / CODE-01** | Documentation, manuscript, index / current status reconciliation | [#66: executable docs and manuscript](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/66): finish truthful API/migration/configuration guidance, generated signposts, 0.4.0 parity, Git-derived metadata fixed point and source-current GitNexus. | Coordinator-only full eleven-group producer, independent whole-output custody/snapshot, real native PDF/figure inspection, generator drift checks and fresh ROOT inventory (currently 142+7). Retain producer manifest/inventory/variables/figure-registry JSON or state hosted upload limits. No old-output or default-seven-tier passing claim. Depends on stable R04-03 source. | +| **R04-06** | CI / starting-main workflows passed; artifact audit separate | [#67: complete hosted evidence](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/67): run declared workflows once on the reviewed candidate and audit actual receipts/artifact bytes. | Required unit/integration/performance/system/slow/H3 matrix on 3.11/3.12, native Windows and early contracts; actual profile/inventory reconciliation, all 46 floors, wheels/paired/ROOT/models/security and complete uploaded artifacts. Derive fresh counts; preserve failures. Depends on R04-01 through R04-05. | +| **R04-07** | Git/main / integrated; verify modernization publication | [#68: accepted main integration](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/68): when implementation is resumed and candidate GO exists, recheck remote/protection/ancestry, integrate ordinarily and verify exact published/main-triggered evidence. | No force-push, ancestry rewrite, stale-SHA acceptance or protection bypass; preserve rollback and unrelated work. Main SHA parity plus actual same-SHA hosted results. Depends on R04-03 through R04-06. | | **SPACE-01** | SPACE / owner-deferred GPU work | [#69: physical backend validation](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/69). GPU experiments are on hold; future work must be usable from a Mac laptop. | Establish a backend compatible with the Mac target and the declared precision contract before physical numerical parity, failure/chunk boundaries, memory and cold/warm timings. Preserve laptop CPU operation; H3 remains host CPU. No CPU-fallback hardware claim. | | **PLACE-V14** | PLACE / deferred licensed data | [#70: complete Cascadia boundary](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/70). | Authorized source/license/extent/resource budget, provenance/checksum/WGS84/stable identities and real renderer/integration; missing layer remains explicit and fail-closed. | -| **PLACE-04** | PLACE / deferred native Windows worker proof | [#71: real Windows download worker](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/71). | Locked PLACE runtime, stalled-header/slow-drip deadlines, termination/pipe closure, batch failure and replay. Shared-process Windows CI does not close this separate item. | +| **PLACE-04** | PLACE / native Windows workflow passed; retained receipt audit separate | [#71: real Windows download worker](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/71). | Locked PLACE runtime, stalled-header/slow-drip deadlines, termination/pipe closure, batch failure and replay. Shared-process Windows CI does not close this separate item. | | **TEST-GNN-01** | SPACE/TIME/ACT / historical cause unresolved | [#72: PROJ SQLite I/O causality](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/72). | Bounded evidence-driven reproduction with version/import/file state; respect prior negative lock probes and distinguish non-recurrence from explanation. Do not suppress CRS tests. | | **TEST-04** | TEST / deferred configured-advisor review | [#73: advisor review](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/73). | Human-owned account/authentication decisions; actual configured-service review bound to the candidate when available. No auth/key/model/billing/hook changes or review claim from service errors. | | **REL-04 / REL-01** | Release / separate future authorization | [#74: 0.4.0 release decision and execution](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/74). | Explicit tag/release/attachment/registry authorization after exact-main readiness; verify destination bytes and retain rollback. Main preparation does not authorize publication or deployment. | diff --git a/pyproject.toml b/pyproject.toml index b43bbafd1..3af009250 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ bio = [ # Health applications health = [ "lifelines>=0.26.0", - # migrate from pymc3 (theano-pymc) to pymc>=4 (aesara/aeppl stack) + # PyMC 5 uses the PyTensor runtime pinned by the bayesian profile. "pymc>=5.0.0", ] From 5f36773b9c054f80d910c698e8fd868ea95438ca Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 09:15:43 -0700 Subject: [PATCH 02/17] Refresh generator-owned manuscript metadata for modernization --- manuscript/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuscript/config.yaml b/manuscript/config.yaml index ece17405e..09da9f0ac 100644 --- a/manuscript/config.yaml +++ b/manuscript/config.yaml @@ -10,7 +10,7 @@ paper: image: "output/figures/graphical_abstract.png" alt: "Graphical abstract: modules grouped into themes composing into geospatial and inference models, verified by an evidence loop into executable claims." version: "0.4.0" # generator-owned (PROJECT_VERSION) - date: "2026-10-07T22:02:24-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) + date: "2026-10-08T09:13:23-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) authors: - name: "Daniel Ari Friedman" From 74f188dceb70f25ff0a12452dcc45e971b7f25e9 Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 09:41:40 -0700 Subject: [PATCH 03/17] refactor(ci): share locked CPU setup and interpreter caches --- .github/actions/sync-cpu-workspace/action.yml | 14 ++++ .github/workflows/ci.yml | 71 ++++++------------- .../tests/unit/test_helper_input_contracts.py | 28 ++++++-- GEO-INFER-TEST/TESTING.md | 22 ++++++ .../tests/unit/test_ci_workflow_contracts.py | 64 ++++++++++++++++- TODO.md | 5 +- 6 files changed, 143 insertions(+), 61 deletions(-) create mode 100644 .github/actions/sync-cpu-workspace/action.yml diff --git a/.github/actions/sync-cpu-workspace/action.yml b/.github/actions/sync-cpu-workspace/action.yml new file mode 100644 index 000000000..c4ff6f4dd --- /dev/null +++ b/.github/actions/sync-cpu-workspace/action.yml @@ -0,0 +1,14 @@ +name: Sync CPU workspace +description: Install every locked workspace member, extra and dependency group on CPU runners. +runs: + using: composite + steps: + - name: Install locked CPU dependencies + shell: bash + run: | + # These optional native extras require CUDA or incompatible hosted + # VTK/C++ toolchains. All other extras and every dependency group stay + # installed. A cache hit never substitutes for the locked sync. + uv sync --locked --python "$(command -v python)" --all-packages --all-extras --all-groups \ + --no-install-package cupy --no-install-package mayavi \ + --no-install-package vaex --no-install-package vaex-core diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e147a0f9..37e11b7fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,26 +63,15 @@ jobs: with: version: "0.10.7" enable-cache: true - cache-suffix: validate + cache-suffix: cpu-workspace-py3.11 + save-cache: false cache-dependency-glob: | **/pyproject.toml **/uv.lock .python-version - - name: Install locked dependencies - run: | - # CuPy requires a CUDA toolchain when no compatible wheel is - # available; the hosted CPU runners do not provide one. Keep the - # performance extra available to developers while excluding it from - # this CPU-only validation environment. - # Mayavi is an optional Python <3.12 3-D extra whose source build is - # incompatible with the hosted runner's VTK dependency; no CI test - # requires that optional backend. - # Vaex is an optional Python <3.12 performance extra whose C++ - # extension does not build against the hosted runner toolchain. - uv sync --locked --python "$(command -v python)" --all-packages --all-extras --all-groups \ - --no-install-package cupy --no-install-package mayavi \ - --no-install-package vaex --no-install-package vaex-core + - name: Install locked workspace dependencies + uses: ./.github/actions/sync-cpu-workspace - name: Provision compatible DuckDB Spatial runtime run: | @@ -256,26 +245,15 @@ jobs: with: version: "0.10.7" enable-cache: true - cache-suffix: fast-contracts + cache-suffix: cpu-workspace-py3.11 + save-cache: false cache-dependency-glob: | **/pyproject.toml **/uv.lock .python-version - - name: Install locked dependencies - run: | - # CuPy requires a CUDA toolchain when no compatible wheel is - # available; the hosted CPU runners do not provide one. Keep the - # performance extra available to developers while excluding it from - # this CPU-only validation environment. - # Mayavi is an optional Python <3.12 3-D extra whose source build is - # incompatible with the hosted runner's VTK dependency; no CI test - # requires that optional backend. - # Vaex is an optional Python <3.12 performance extra whose C++ - # extension does not build against the hosted runner toolchain. - uv sync --locked --python "$(command -v python)" --all-packages --all-extras --all-groups \ - --no-install-package cupy --no-install-package mayavi \ - --no-install-package vaex --no-install-package vaex-core + - name: Install locked workspace dependencies + uses: ./.github/actions/sync-cpu-workspace - name: Provision compatible DuckDB Spatial runtime run: | @@ -485,26 +463,17 @@ jobs: with: version: "0.10.7" enable-cache: true - cache-suffix: py${{ matrix.python-version }}-${{ matrix.category }} + cache-suffix: cpu-workspace-py${{ matrix.python-version }} + # One complete workspace writer per interpreter; other categories + # restore the same lock-bound download cache without save races. + save-cache: ${{ matrix.category == 'unit' }} cache-dependency-glob: | **/pyproject.toml **/uv.lock .python-version - - name: Install locked dependencies - run: | - # CuPy requires a CUDA toolchain when no compatible wheel is - # available; the hosted CPU runners do not provide one. Keep the - # performance extra available to developers while excluding it from - # this CPU-only validation environment. - # Mayavi is an optional Python <3.12 3-D extra whose source build is - # incompatible with the hosted runner's VTK dependency; no CI test - # requires that optional backend. - # Vaex is an optional Python <3.12 performance extra whose C++ - # extension does not build against the hosted runner toolchain. - uv sync --locked --python "$(command -v python)" --all-packages --all-extras --all-groups \ - --no-install-package cupy --no-install-package mayavi \ - --no-install-package vaex --no-install-package vaex-core + - name: Install locked workspace dependencies + uses: ./.github/actions/sync-cpu-workspace - name: Provision compatible DuckDB Spatial runtime run: | @@ -582,13 +551,15 @@ jobs: with: version: "0.10.7" enable-cache: true - cache-suffix: manuscript + cache-suffix: cpu-workspace-py3.11 + save-cache: false + cache-dependency-glob: | + **/pyproject.toml + **/uv.lock + .python-version - name: Install locked workspace dependencies - run: | - uv sync --locked --python "$(command -v python)" --all-packages --all-extras --all-groups \ - --no-install-package cupy --no-install-package mayavi \ - --no-install-package vaex --no-install-package vaex-core + uses: ./.github/actions/sync-cpu-workspace - name: Generate research artifacts run: | diff --git a/GEO-INFER-SPM/tests/unit/test_helper_input_contracts.py b/GEO-INFER-SPM/tests/unit/test_helper_input_contracts.py index 6bcb0d14e..269074b0e 100644 --- a/GEO-INFER-SPM/tests/unit/test_helper_input_contracts.py +++ b/GEO-INFER-SPM/tests/unit/test_helper_input_contracts.py @@ -2,6 +2,7 @@ import subprocess import sys +import inspect import numpy as np import pytest @@ -73,24 +74,37 @@ def test_basis_count_is_positive_integer(count): def test_polynomial_default_terminates_and_matches_total_degree_oracle(): - # A bounded real child protects the suite against the former infinite loop - # for the public default n_basis=10. + # Execute the production function's definition with real NumPy in a bounded + # child. Importing the entire SPM package in that child would also initialize + # optional samplers, conflating their startup with this algorithm's budget. code = """ import numpy as np -from geo_infer_spm.utils.helpers import create_spatial_basis_functions +import sys +namespace = {"np": np} +exec("from __future__ import annotations\\n" + sys.argv[1], namespace) coords = np.array([[-1., -2.], [0., 0.], [1., 2.]]) x, y = ((coords - coords.mean(axis=0)) / coords.std(axis=0)).T expected = np.column_stack([np.ones(3), y, x, y*y, x*y, x*x, y**3, x*y*y, x*x*y, x**3]) -actual = create_spatial_basis_functions(coords, method="polynomial") +actual = namespace["create_spatial_basis_functions"](coords, method="polynomial") np.testing.assert_allclose(actual, expected) """ - subprocess.run( - [sys.executable, "-c", code], - check=True, + completed = subprocess.run( + [sys.executable, "-c", code, inspect.getsource(create_spatial_basis_functions)], timeout=20, capture_output=True, text=True, ) + assert completed.returncode == 0, completed.stderr or completed.stdout + # The public module path is already imported in the parent; check its actual + # callable as well as the bounded production-definition execution above. + coords = np.array([[-1.0, -2.0], [0.0, 0.0], [1.0, 2.0]]) + x, y = ((coords - coords.mean(axis=0)) / coords.std(axis=0)).T + expected = np.column_stack( + [np.ones(3), y, x, y * y, x * y, x * x, y**3, x * y * y, x * x * y, x**3] + ) + np.testing.assert_allclose( + create_spatial_basis_functions(coords, method="polynomial"), expected + ) @pytest.mark.parametrize( diff --git a/GEO-INFER-TEST/TESTING.md b/GEO-INFER-TEST/TESTING.md index 17c0b56c8..8a4531796 100644 --- a/GEO-INFER-TEST/TESTING.md +++ b/GEO-INFER-TEST/TESTING.md @@ -52,3 +52,25 @@ Model checks require finite outputs, declared shapes and dtypes, normalized prob suite's failing test names in the final summary verdict (summary.json records them regardless), and inspect the corresponding JUnit XML and `summary.json` under `.geo-infer-test-results/`. + +## CI setup and timing + +The CPU validation, fast-contract, category and manuscript jobs use +`.github/actions/sync-cpu-workspace/action.yml` for the same locked workspace +sync. It installs all members, extras and dependency groups with the four +explicit hosted CPU exclusions (CuPy, Mayavi, Vaex and Vaex Core). Each job +constructs its own environment; a cache hit never replaces `uv sync --locked`. + +These jobs share the uv download/build cache per Python interpreter, keyed by +all workspace manifests, the lock and `.python-version`. Only each interpreter's +unit job saves that key; the other jobs restore it without competing uploads. +The installed-wheel job retains its separate cache and installation isolation. + +The baseline green run [37734618009](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734618009) +spent 65–106 seconds in each category's dependency sync. System tests took +15–16 seconds and unit tests 494–525 seconds. Compare the same step timings in +the next green run before claiming a speedup. A new cache key still starts cold; +no jobs wait for another category to populate it. Workflow contracts parse each +workflow once and return independent document copies to prevent test coupling. +Per-module execution receipts and `--durations=10` retain expensive runtime paths +for subsequent profiling without narrowing selections or relaxing deadlines. diff --git a/GEO-INFER-TEST/tests/unit/test_ci_workflow_contracts.py b/GEO-INFER-TEST/tests/unit/test_ci_workflow_contracts.py index b01bff7e9..201f9479b 100644 --- a/GEO-INFER-TEST/tests/unit/test_ci_workflow_contracts.py +++ b/GEO-INFER-TEST/tests/unit/test_ci_workflow_contracts.py @@ -7,7 +7,9 @@ from __future__ import annotations +import copy import importlib.util +from functools import cache import re import sys from pathlib import Path @@ -18,12 +20,19 @@ WORKFLOWS = REPO_ROOT / ".github" / "workflows" -def _load(name: str) -> dict: +@cache +def _parsed_workflow(name: str) -> dict: document = yaml.safe_load((WORKFLOWS / name).read_text(encoding="utf-8")) assert isinstance(document, dict) return document +def _load(name: str) -> dict: + # Parse each immutable workflow once per test process. Return an independent + # copy so a test cannot mutate the document seen by another contract. + return copy.deepcopy(_parsed_workflow(name)) + + def _trigger(document: dict) -> dict: # PyYAML resolves a bare `on:` key to the boolean True (YAML 1.1). return document.get("on", document.get(True, {})) @@ -498,10 +507,11 @@ def test_workspace_syncs_install_every_dependency_group(): commands = [ command for workflow in sorted(WORKFLOWS.glob("*.yml")) + + sorted((REPO_ROOT / ".github/actions").glob("*/action.yml")) for command in _workspace_sync_commands(workflow.read_text(encoding="utf-8")) if "--all-packages" in command ] - assert len(commands) >= 6 + assert len(commands) >= 3 for command in commands: tokens = command.split() assert {"--locked", "--all-extras", "--all-groups"} <= set(tokens), command @@ -616,7 +626,7 @@ def test_fast_and_validation_jobs_retain_full_attempt_artifacts(): assert "needs" not in jobs["fast-contracts"] fast = _dump(jobs["fast-contracts"]) assert "--paths" in fast and "test_space_time_composition_contract.py" in fast - assert "uv sync --locked" in fast and "--all-extras" in fast + assert "./.github/actions/sync-cpu-workspace" in fast def test_process_tree_contract_has_real_hosted_windows_execution(): @@ -683,3 +693,51 @@ def test_cpu_jobs_explicitly_provision_real_duckdb_spatial_runtime(): def test_called_and_standalone_pair_jobs_have_distinct_concurrency_groups(): group = _load("gnn-interchange.yml")["concurrency"]["group"] assert "github.workflow" in group and "github.event_name" in group + + +def test_cpu_workspace_setup_is_shared_and_remains_fail_closed(): + jobs = _load("ci.yml")["jobs"] + for name in ("validate", "fast-contracts", "test", "manuscript"): + steps = jobs[name]["steps"] + sync = [ + step + for step in steps + if step.get("uses") == "./.github/actions/sync-cpu-workspace" + ] + assert len(sync) == 1, name + assert "continue-on-error" not in sync[0] + assert not _workspace_sync_commands(_dump(jobs[name])) + setup_index = next( + i + for i, step in enumerate(steps) + if step.get("uses", "").startswith("astral-sh/setup-uv@") + ) + assert steps.index(sync[0]) > setup_index + + +def test_cpu_cache_has_one_writer_per_interpreter_and_no_environment_reuse(): + jobs = _load("ci.yml")["jobs"] + for name in ("validate", "fast-contracts", "test", "manuscript"): + setup = next( + step + for step in jobs[name]["steps"] + if step.get("uses", "").startswith("astral-sh/setup-uv@") + )["with"] + expected = ( + "cpu-workspace-py${{ matrix.python-version }}" + if name == "test" + else "cpu-workspace-py3.11" + ) + assert setup["cache-suffix"] == expected + assert setup["enable-cache"] is True + assert setup["save-cache"] == ( + "${{ matrix.category == 'unit' }}" if name == "test" else False + ) + assert setup["cache-dependency-glob"].splitlines() == [ + "**/pyproject.toml", + "**/uv.lock", + ".python-version", + ] + # Only the uv package download/build cache is reused; the composite + # action still constructs and synchronizes each job's environment. + assert ".venv" not in setup.get("cache-local-path", "") diff --git a/TODO.md b/TODO.md index c6bc48fe9..6c81e775c 100644 --- a/TODO.md +++ b/TODO.md @@ -56,7 +56,10 @@ The modernization pass replaces obsolete H3 setup guidance with supported APIs, corrects RNG documentation, and repairs SPM helper input rejection, silently omitted formula interactions, nonfinite degenerate bases and the default polynomial basis's nonterminating expansion. Migration guidance records the -new polynomial ordering and explicit supported formula grammar. Working public +new polynomial ordering and explicit supported formula grammar. CI now shares a +locked CPU workspace sync and per-interpreter uv package cache, with one writer +per key and isolated job environments. Workflow tests reuse parsed YAML through +independent copies; the full 35-contract selection passed. Working public API names, security rejection tests, publisher URLs and hash-bound historical identities remain meaningful contracts rather than removal targets. From 5b14314f20b0574a3dd6f7895315f89c280d9080 Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 09:49:24 -0700 Subject: [PATCH 04/17] fix(manuscript): stop verification admission after interruption --- GEO-INFER-TEST/rewrite_readme_agents.py | 15 ++++++++++ manuscript/AGENTS.md | 11 +++++++ manuscript/README.md | 11 +++++++ manuscript/generate_research_artifacts.py | 8 +++-- tests/test_manuscript_verification_table.py | 33 +++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/GEO-INFER-TEST/rewrite_readme_agents.py b/GEO-INFER-TEST/rewrite_readme_agents.py index 70a41bf77..8989d725d 100755 --- a/GEO-INFER-TEST/rewrite_readme_agents.py +++ b/GEO-INFER-TEST/rewrite_readme_agents.py @@ -299,6 +299,19 @@ def validation_commands(path: Path, module: ModuleInfo | None) -> str: def module_readme_notes(path: Path, module: ModuleInfo | None) -> str: """Return implemented module-specific README notes for module roots.""" + if path.parent == REPO_ROOT / "manuscript": + return """ +## Research Verification + +- `uv run python manuscript/generate_research_artifacts.py --verify` runs the + producer's verification groups and retains immutable execution receipts. +- Ordinary failures and timeouts continue the diagnostic sweep. An interruption + retains its failed attempt and stops admission; later groups remain visibly + not run. An incomplete sweep cannot establish acceptance. +- Add `--full-validation` for the unit, integration, performance and H3 groups. + Run ROOT manuscript and manuscript-render profiles separately; the render + profile requires the actual generated PDF. +""" if not module or path.parent != module.path: return "" if module.name == "GEO-INFER-TEST": @@ -566,6 +579,8 @@ def module_readme_notes(path: Path, module: ModuleInfo | None) -> str: def module_agent_notes(path: Path, module: ModuleInfo | None) -> str: """Return implemented module-specific AGENTS notes for module roots.""" + if path.parent == REPO_ROOT / "manuscript": + return module_readme_notes(path, module) if not module or path.parent != module.path: return "" if module.name == "GEO-INFER-TEST": diff --git a/manuscript/AGENTS.md b/manuscript/AGENTS.md index c777192c4..adfefb16a 100644 --- a/manuscript/AGENTS.md +++ b/manuscript/AGENTS.md @@ -45,6 +45,17 @@ uv run python GEO-INFER-TEST/validate_repo_contracts.py --skip-import-smoke ``` +## Research Verification + +- `uv run python manuscript/generate_research_artifacts.py --verify` runs the + producer's verification groups and retains immutable execution receipts. +- Ordinary failures and timeouts continue the diagnostic sweep. An interruption + retains its failed attempt and stops admission; later groups remain visibly + not run. An incomplete sweep cannot establish acceptance. +- Add `--full-validation` for the unit, integration, performance and H3 groups. + Run ROOT manuscript and manuscript-render profiles separately; the render + profile requires the actual generated PDF. + ## Integration Notes - Update this AGENTS.md and the sibling README.md when commands, exports, dependencies, or generated outputs change. diff --git a/manuscript/README.md b/manuscript/README.md index 1572c0da7..653dc7100 100644 --- a/manuscript/README.md +++ b/manuscript/README.md @@ -53,6 +53,17 @@ uv run python GEO-INFER-TEST/validate_repo_contracts.py --skip-import-smoke ``` +## Research Verification + +- `uv run python manuscript/generate_research_artifacts.py --verify` runs the + producer's verification groups and retains immutable execution receipts. +- Ordinary failures and timeouts continue the diagnostic sweep. An interruption + retains its failed attempt and stops admission; later groups remain visibly + not run. An incomplete sweep cannot establish acceptance. +- Add `--full-validation` for the unit, integration, performance and H3 groups. + Run ROOT manuscript and manuscript-render profiles separately; the render + profile requires the actual generated PDF. + ## Documentation Notes This README describes current repository state only. Keep examples and claims tied to importable code, tracked files, or validation commands. diff --git a/manuscript/generate_research_artifacts.py b/manuscript/generate_research_artifacts.py index 53cc6261c..b23c54478 100644 --- a/manuscript/generate_research_artifacts.py +++ b/manuscript/generate_research_artifacts.py @@ -2018,9 +2018,11 @@ def run_verification( Shared execution provides monotonic deadlines, process-tree cleanup and immutable attempts, including retained output and artifact hashes. Each result links to that receipt relative to ``root``; its bounded output tail - is a summary. Legacy records without receipt references remain readable. + is a summary. Earlier records without receipt references remain readable. A timeout or ordinary failure is recorded without retrying or aborting the - remaining groups. Importing the generator does not initialize execution. + remaining groups. An interrupted attempt retains its failure and stops + admission; the remaining groups are reported as not run. Importing the + generator does not initialize execution. """ from geo_infer_test.execution import run_command @@ -2072,6 +2074,8 @@ def run_verification( ), ) ) + if completed.status == "INTERRUPTED": + break return tuple(results) diff --git a/tests/test_manuscript_verification_table.py b/tests/test_manuscript_verification_table.py index d973db274..f69f7b345 100644 --- a/tests/test_manuscript_verification_table.py +++ b/tests/test_manuscript_verification_table.py @@ -435,6 +435,39 @@ def test_run_verification_continues_after_a_failing_first_group( assert results[1].status == "passed" assert results[1].return_code == 0 + def test_interruption_retains_failure_and_leaves_later_groups_not_run( + self, generator: ModuleType, tmp_path: Path, monkeypatch + ) -> None: + monkeypatch.setattr( + generator, + "VERIFICATION_COMMANDS", + (("cancelled", "true"), ("must-not-launch", "true")), + ) + admitted = [] + + def interrupted(command, name, timeout, **_kwargs): + admitted.append(name) + assert name == "cancelled", "interruption admitted another group" + return execution.CommandResult( + name, + False, + 1.0, + command, + status="INTERRUPTED", + stderr="interrupted; cleanup receipt retained", + returncode=None, + receipt=str(tmp_path / "attempt" / "receipt.json"), + ) + + monkeypatch.setattr(execution, "run_command", interrupted) + results = generator.run_verification(tmp_path) + assert admitted == ["cancelled"] + assert len(results) == 1 and results[0].status == "failed" + assert results[0].receipt == "attempt/receipt.json" + assert "cleanup receipt retained" in results[0].output_tail + table = generator._verification_table(results, full_validation=False) + assert "| `must-not-launch` | `true` | not run |" in table + def test_compilation_expands_only_its_paths_against_execution_root( self, generator: ModuleType, tmp_path: Path ) -> None: From d1c993e274ec2d52d2c44f721d697b7d82c5ce76 Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 09:50:43 -0700 Subject: [PATCH 05/17] Refresh producer metadata after CI and cancellation changes --- manuscript/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuscript/config.yaml b/manuscript/config.yaml index 09da9f0ac..0a633efe5 100644 --- a/manuscript/config.yaml +++ b/manuscript/config.yaml @@ -10,7 +10,7 @@ paper: image: "output/figures/graphical_abstract.png" alt: "Graphical abstract: modules grouped into themes composing into geospatial and inference models, verified by an evidence loop into executable claims." version: "0.4.0" # generator-owned (PROJECT_VERSION) - date: "2026-10-08T09:13:23-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) + date: "2026-10-08T09:49:24-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) authors: - name: "Daniel Ari Friedman" From 71e7e598329be0ed7d84e9ba220bf4963239334c Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 09:54:47 -0700 Subject: [PATCH 06/17] Clarify supported trend and version input contracts --- .../src/geo_infer_art/core/generation/custom_algorithms.py | 2 +- GEO-INFER-SPACE/src/geo_infer_space/backends/h3/h3_backend.py | 2 +- GEO-INFER-TIME/src/geo_infer_time/core/analysis.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GEO-INFER-ART/src/geo_infer_art/core/generation/custom_algorithms.py b/GEO-INFER-ART/src/geo_infer_art/core/generation/custom_algorithms.py index 1ac5ed9f8..86a5379ea 100644 --- a/GEO-INFER-ART/src/geo_infer_art/core/generation/custom_algorithms.py +++ b/GEO-INFER-ART/src/geo_infer_art/core/generation/custom_algorithms.py @@ -202,7 +202,7 @@ def load_algorithms_from_file(self, filepath: str) -> None: """Load schema version 1 using only the trusted callable registry. Validation is atomic: malformed metadata, duplicate names, unsupported - versions, source-bearing legacy files and unknown keys raise + versions, source-bearing files and unknown keys raise ``ValueError`` without registering any entries. No persisted value is passed to import, exec or eval. """ diff --git a/GEO-INFER-SPACE/src/geo_infer_space/backends/h3/h3_backend.py b/GEO-INFER-SPACE/src/geo_infer_space/backends/h3/h3_backend.py index c3985e091..d46be0b2e 100644 --- a/GEO-INFER-SPACE/src/geo_infer_space/backends/h3/h3_backend.py +++ b/GEO-INFER-SPACE/src/geo_infer_space/backends/h3/h3_backend.py @@ -55,7 +55,7 @@ def _disk_budget(cell: str, k: int, max_cells: int, *, minimum: int = 0) -> None def _version_tuple(version: str) -> tuple[int, int, int] | None: - """Parse a semantic H3 version without accepting a legacy major release.""" + """Parse a semantic H3 version without accepting an unsupported major release.""" try: parts = version.lstrip("v").split(".") return cast( diff --git a/GEO-INFER-TIME/src/geo_infer_time/core/analysis.py b/GEO-INFER-TIME/src/geo_infer_time/core/analysis.py index 08ae63a1b..329fc94df 100644 --- a/GEO-INFER-TIME/src/geo_infer_time/core/analysis.py +++ b/GEO-INFER-TIME/src/geo_infer_time/core/analysis.py @@ -74,7 +74,7 @@ def detect_trend( Returns: Dictionary with trend information. Linear ``slope_per_sample`` - and the legacy ``trend_strength`` describe change per observation; + and ``trend_strength`` describe change per observation; ``r_squared`` measures fit quality. Numerical constants are stable. """ if method not in {"linear", "polynomial", "moving_average"}: From 416713907b6edc73f7c7c2ae4d7389e5ff074b7b Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 09:55:35 -0700 Subject: [PATCH 07/17] Refresh producer metadata after source documentation cleanup --- manuscript/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuscript/config.yaml b/manuscript/config.yaml index 0a633efe5..b2cc6d2d6 100644 --- a/manuscript/config.yaml +++ b/manuscript/config.yaml @@ -10,7 +10,7 @@ paper: image: "output/figures/graphical_abstract.png" alt: "Graphical abstract: modules grouped into themes composing into geospatial and inference models, verified by an evidence loop into executable claims." version: "0.4.0" # generator-owned (PROJECT_VERSION) - date: "2026-10-08T09:49:24-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) + date: "2026-10-08T09:54:47-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) authors: - name: "Daniel Ari Friedman" From 2d1211413610acc2c3f70328342f963a36923fac Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 10:33:14 -0700 Subject: [PATCH 08/17] Refresh producer metadata after accepted GNN main integration --- manuscript/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuscript/config.yaml b/manuscript/config.yaml index b2cc6d2d6..b475fad05 100644 --- a/manuscript/config.yaml +++ b/manuscript/config.yaml @@ -10,7 +10,7 @@ paper: image: "output/figures/graphical_abstract.png" alt: "Graphical abstract: modules grouped into themes composing into geospatial and inference models, verified by an evidence loop into executable claims." version: "0.4.0" # generator-owned (PROJECT_VERSION) - date: "2026-10-08T09:54:47-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) + date: "2026-10-08T10:32:29-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) authors: - name: "Daniel Ari Friedman" From ec394942ba722b626878cfe55c08db8ef3aae93d Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 10:47:49 -0700 Subject: [PATCH 09/17] Add finite circulation and graph Hodge contracts with issue reconciliation --- CHANGELOG.md | 15 ++ GEO-INFER-INTRA/docs/AGENTS.md | 1 + GEO-INFER-INTRA/docs/README.md | 1 + GEO-INFER-INTRA/docs/math/circulation.md | 61 +++++ .../releases/issue_reconciliation_2026_10.md | 34 +++ .../src/geo_infer_math/core/AGENTS.md | 1 + .../src/geo_infer_math/core/README.md | 15 +- .../src/geo_infer_math/core/__init__.py | 2 + .../src/geo_infer_math/core/circulation.py | 219 ++++++++++++++++++ GEO-INFER-MATH/tests/unit/AGENTS.md | 1 + GEO-INFER-MATH/tests/unit/README.md | 1 + GEO-INFER-MATH/tests/unit/test_circulation.py | 143 ++++++++++++ GEO-INFER-TEST/doc_examples.json | 1 + README.md | 6 +- TODO.md | 3 + 15 files changed, 494 insertions(+), 10 deletions(-) create mode 100644 GEO-INFER-INTRA/docs/math/circulation.md create mode 100644 GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md create mode 100644 GEO-INFER-MATH/src/geo_infer_math/core/circulation.py create mode 100644 GEO-INFER-MATH/tests/unit/test_circulation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 34946d044..b725c7260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 0.4.0 candidate preparation +- Add MATH finite-real skew decomposition, transition/rate probability currents, + detailed-balance checks and discrete graph Hodge projections with an explicit + harmonic remainder. Analytical tests cover nonuniform stationary measures, + irreversible cycles, face/no-face topology, disconnected gauges and invalid + inputs; these numerical anchors do not claim external formal proof acceptance. +- Repair SPM formula rejection, degenerate radial/Gaussian bases and the default + polynomial basis expansion; document supported grammar and polynomial order. +- Share locked CPU workspace installation and interpreter-specific uv package + caches across CI jobs, with one cache writer per interpreter. Cache parsed + workflow fixtures while returning independent copies to each contract test. +- Stop manuscript verification admission after interruption, retaining the + interrupted attempt and explicit not-run groups. +- Replace obsolete H3 installation/API examples and unsupported RNG docstrings + with current uv, H3 v4 and generator contracts. + - Measure owning TEST source files named `test_*` instead of excluding them by filename, with regression coverage for each declared coverage configuration. - Propagate ownership census timeouts separately from target pipe polling and diff --git a/GEO-INFER-INTRA/docs/AGENTS.md b/GEO-INFER-INTRA/docs/AGENTS.md index 1d3a16a1c..05cf4ee12 100644 --- a/GEO-INFER-INTRA/docs/AGENTS.md +++ b/GEO-INFER-INTRA/docs/AGENTS.md @@ -34,6 +34,7 @@ - `integration/` - `knowledge_base/` - `materiality/` +- `math/` - `modules/` - `ontology/` - `realms/` diff --git a/GEO-INFER-INTRA/docs/README.md b/GEO-INFER-INTRA/docs/README.md index 193ba10ac..f8ce7e2d9 100644 --- a/GEO-INFER-INTRA/docs/README.md +++ b/GEO-INFER-INTRA/docs/README.md @@ -17,6 +17,7 @@ Docs workspace within `GEO-INFER-INTRA`. - `integration/` - `knowledge_base/` - `materiality/` +- `math/` - `modules/` - `ontology/` - `realms/` diff --git a/GEO-INFER-INTRA/docs/math/circulation.md b/GEO-INFER-INTRA/docs/math/circulation.md new file mode 100644 index 000000000..91d597d0c --- /dev/null +++ b/GEO-INFER-INTRA/docs/math/circulation.md @@ -0,0 +1,61 @@ +# Finite probability currents and graph decomposition + +The owning API is `geo_infer_math.core.circulation`. These are finite real +numerical operations, with input validation and no implicit normalization. +They provide implemented anchors for [issue #58](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/58); +external notation-bridge integration and formal proof acceptance are separate. + +`skew_part(A)` returns `(A - A.T)/2`. Its trace and real quadratic form vanish; +adding the symmetric part reconstructs the input. Scaling before subtraction +preserves finite opposite-sign entries near the floating-point limit. + +`probability_current(K, pi, kind="transition")` accepts a nonnegative +row-stochastic transition matrix and a normalized nonnegative probability vector. +`kind="rate"` instead requires nonnegative off-diagonals, nonpositive diagonals +and zero row sums. State order is preserved. The vector need not be stationary. +The returned current is `J[i,j] = pi[i] K[i,j] - pi[j] K[j,i]`. +`circulation_part` returns half that current; `satisfies_detailed_balance` +checks each current entry against an explicit absolute tolerance. + +`current_divergence(J)[j] = sum_i J[i,j]` uses the incoming convention. +For transition kernels this equals `pi @ K - pi`; for rate kernels it equals +`pi @ K`. Thus stationarity gives zero divergence but does not imply detailed +balance: a directed three-state cycle has a nonzero divergence-free current. +Dropping the circulation loses that current. + +```python +import numpy as np +from geo_infer_math.core.circulation import ( + probability_current, current_divergence, satisfies_detailed_balance, + graph_hodge_decomposition, +) + +P = np.array([[0., 1., 0.], [0., 0., 1.], [1., 0., 0.]]) +pi = np.full(3, 1/3) +J = probability_current(P, pi) +assert np.allclose(current_divergence(J), 0) +assert not satisfies_detailed_balance(P, pi) + +B = np.array([[-1., 0., 1.], [1., -1., 0.], [0., 1., -1.]]) +parts = graph_hodge_decomposition(B, [2., -1., 5.], np.ones((3, 1))) +assert np.allclose(parts.gradient, [0., -3., 3.]) +assert np.allclose(parts.solenoidal, [2., 2., 2.]) +assert np.allclose(parts.harmonic, 0) +``` + +The unweighted discrete graph contract uses incidence `B` (nodes by edges), with +one -1 source and +1 target per edge. Optional face boundary `C` (edges by faces) +must satisfy `B @ C = 0`. Least-squares projections return gradient in +`range(B.T)`, face-curl (`solenoidal`) in `range(C)` and the explicit harmonic +remainder. The remainder lies in `ker(B)` and `ker(C.T)` up to numerical error. +Node and face potentials use the minimum-norm gauge. Disconnected and edgeless +graphs are supported. Without faces, the face-curl is zero and cycle flow is +reported as harmonic. The total divergence-free component is the sum of +`solenoidal` and `harmonic`; the names depend on the supplied finite complex. +This API does not solve continuum PDE boundary conditions or infer faces. + +Verify with: + +```bash +uv run python GEO-INFER-TEST/run_unified_tests.py --paths GEO-INFER-MATH/tests/unit/test_circulation.py +``` diff --git a/GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md b/GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md new file mode 100644 index 000000000..bf9e6c192 --- /dev/null +++ b/GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md @@ -0,0 +1,34 @@ +# Issue reconciliation, 2026-10-08 + +The owner requested repository improvements, issue/PR reconciliation and ordinary +integration into main. Closing an issue means its actual requirements are met; +archiving a deferred issue as not planned would preserve its requirements in +`TODO.md` and would not constitute technical acceptance. + +The earlier readiness issues describe pre-integration checkpoints. Main +`c1027a9d538f4656ed854f2735874f0e0782686c` incorporates the accepted GNN 4.1.0 +pin; the modernization branch preserves that ancestry. Successful hosted CI is +useful current evidence but does not resolve historical causal questions, +independent review, local native acceptance or external prerequisites. + +| Issue | Implemented work / current evidence | Remaining acceptance | +| --- | --- | --- | +| #58 | MATH `core.circulation`: skew part, transition/rate currents, detailed balance, incoming divergence, finite graph Hodge projections and explicit harmonic remainder; 28 analytical cases and 351 MATH unit cases pass locally. | Final candidate hosted checks and main integration. External formal-repository bridge edits are separate. | +| #61 | Starting main and intermediate modernization candidates have green hosted gates; obsolete pre-integration status is superseded. | Required child acceptance and exact final main evidence. | +| #62 | Phase-aware deadline and census diagnostics are implemented; hosted ordinary TEST succeeds on recent candidates. | Historical timeout attribution and fresh independent infrastructure review remain unresolved; recurrence-free runs alone are not causal proof. | +| #63 | Genuine TEST coverage is included in the unchanged 85% floor gate; recent hosted validation succeeds. | Audit the final candidate's complete 46 dispositions and retained behavior identities. | +| #64 | Analytical runtime changes are implemented, focused oracles and broad hosted matrix run. | All 45 strict imports now pass locally; full local suite acceptance still has retained timeout/census failures on this Mac. Fresh independent review is unavailable. | +| #65 | Two-interpreter installed-wheel and paired interchange lanes execute; GNN pin is the accepted 4.1.0 source, superseding the issue's older pin. | Complete final wheel/profile/interchange artifact audit and independent boundary review. | +| #66 | Executable docs, generated signposts, changelog and producer cancellation handling updated. | Local full-eleven producer acceptance, whole-output custody, final index and independent scientific/native render review remain distinct from hosted default-tier manuscript success. | +| #67 | Recent candidate hosted matrix and native Windows/portability/interchange lanes pass. | Final candidate workflows and every expected artifact audit; whole-tree format dispatch. | +| #68 | Concurrent GNN pin is incorporated through an ordinary merge; published ancestry preserved. | Final integration and same-SHA main workflows; independent GO is not claimed. | +| #69 | CPU path and documented optional GPU boundary preserved. | Actual physical GPU execution, device/version evidence, cold/warm timings and memory require hardware. | +| #70 | Existing captured layers/replay preserved. | Complete bioregion acquisition still requires a declared source, license, extent and resource budget. | +| #72 | Historical PROJ evidence and negative induced-lock result retained. | Historical SQLite I/O cause remains UNKNOWN; newer green runs do not identify it. | +| #73 | Account/configuration boundaries preserved. | Configured advisor review requires account-level remedy; no advisor result is claimed. | +| #74 | Source remains uniformly 0.4.0. | Release decision, tag and registry/release publication remain separately authorized actions. Main integration is not a release. | +| #78 | Content-free live owned-process denial facts are retained; recent hosted runs pass. | Historical Linux live-task denial class is not identified. No census weakening or automatic retry was added. | + +Completed implementation, hosted receipts, historical failed attempts and +unmet acceptance criteria must remain distinguishable. Public issue comments +must not include private host inventories, credentials or conversation artifacts. diff --git a/GEO-INFER-MATH/src/geo_infer_math/core/AGENTS.md b/GEO-INFER-MATH/src/geo_infer_math/core/AGENTS.md index 53c5c6b62..66ec3ac29 100644 --- a/GEO-INFER-MATH/src/geo_infer_math/core/AGENTS.md +++ b/GEO-INFER-MATH/src/geo_infer_math/core/AGENTS.md @@ -24,6 +24,7 @@ - `information_theory/` - `theorem_proving/` - `__init__.py` +- `circulation.py` - `geometry.py` - `gpu_acceleration.py` - `graph_theory.py` diff --git a/GEO-INFER-MATH/src/geo_infer_math/core/README.md b/GEO-INFER-MATH/src/geo_infer_math/core/README.md index f34184d7d..4513ab7e0 100644 --- a/GEO-INFER-MATH/src/geo_infer_math/core/README.md +++ b/GEO-INFER-MATH/src/geo_infer_math/core/README.md @@ -7,6 +7,7 @@ Core workspace within `GEO-INFER-MATH`. - `information_theory/` - `theorem_proving/` - `__init__.py` +- `circulation.py` - `geometry.py` - `gpu_acceleration.py` - `graph_theory.py` @@ -20,6 +21,13 @@ Core workspace within `GEO-INFER-MATH`. ## Public Interface +- `circulation.py:skew_part` (function) +- `circulation.py:probability_current` (function) +- `circulation.py:circulation_part` (function) +- `circulation.py:current_divergence` (function) +- `circulation.py:satisfies_detailed_balance` (function) +- `circulation.py:GraphHodgeDecomposition` (class) +- `circulation.py:graph_hodge_decomposition` (function) - `geometry.py:Point` (class) - `geometry.py:LineString` (class) - `geometry.py:Polygon` (class) @@ -33,13 +41,6 @@ Core workspace within `GEO-INFER-MATH`. - `geometry.py:line_intersection` (function) - `geometry.py:polygon_area_spherical` (function) - `geometry.py:great_circle_distance` (function) -- `gpu_acceleration.py:GPUAccelerator` (class) -- `gpu_acceleration.py:is_gpu_available` (function) -- `gpu_acceleration.py:get_gpu_info` (function) -- `gpu_acceleration.py:benchmark_gpu_performance` (function) -- `gpu_acceleration.py:gpu_matrix_multiply` (function) -- `gpu_acceleration.py:gpu_distance_matrix` (function) -- `gpu_acceleration.py:gpu_spatial_interpolation` (function) ## Module Metadata diff --git a/GEO-INFER-MATH/src/geo_infer_math/core/__init__.py b/GEO-INFER-MATH/src/geo_infer_math/core/__init__.py index 7848351d3..456562883 100644 --- a/GEO-INFER-MATH/src/geo_infer_math/core/__init__.py +++ b/GEO-INFER-MATH/src/geo_infer_math/core/__init__.py @@ -10,6 +10,7 @@ """ from geo_infer_math.core import ( + circulation, geometry, gpu_acceleration, graph_theory, @@ -25,6 +26,7 @@ ) __all__ = [ + "circulation", "spatial_statistics", "interpolation", "optimization", diff --git a/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py b/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py new file mode 100644 index 000000000..c651ea39a --- /dev/null +++ b/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py @@ -0,0 +1,219 @@ +"""Finite real matrix, Markov-current and discrete graph Hodge operations. + +These numerical constructs provide explicit arrays and remainder witnesses; +they do not assert continuum boundary conditions or formal theorem acceptance. +""" + +from dataclasses import dataclass + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +__all__ = [ + "skew_part", + "probability_current", + "circulation_part", + "current_divergence", + "satisfies_detailed_balance", + "GraphHodgeDecomposition", + "graph_hodge_decomposition", +] + + +def _real_array(value: ArrayLike, name: str, ndim: int) -> NDArray[np.float64]: + array = np.asarray(value) + if array.ndim != ndim or array.dtype.kind not in "iuf": + raise ValueError(f"{name} must be a real numeric {ndim}-dimensional array") + array = array.astype(np.float64) + if not np.isfinite(array).all(): + raise ValueError(f"{name} must contain only finite values") + return array + + +def _tolerance(atol: float) -> float: + if isinstance(atol, (bool, np.bool_)) or not np.isscalar(atol): + raise ValueError("atol must be a finite nonnegative number") + if not isinstance(atol, (int, float, np.integer, np.floating)): + raise ValueError("atol must be a finite nonnegative number") + if not np.isfinite(atol) or atol < 0: + raise ValueError("atol must be a finite nonnegative number") + return float(atol) + + +def _square(value: ArrayLike, name: str) -> NDArray[np.float64]: + array = _real_array(value, name, 2) + if array.shape[0] == 0 or array.shape[0] != array.shape[1]: + raise ValueError(f"{name} must be a nonempty square matrix") + return array + + +def skew_part(matrix: ArrayLike) -> NDArray[np.float64]: + """Return (A - A.T)/2 for a finite real square matrix, without mutation.""" + array = _square(matrix, "matrix") + # Scaling first avoids overflow when subtracting opposite large entries. + return array * 0.5 - array.T * 0.5 + + +def probability_current( + kernel: ArrayLike, + probability: ArrayLike, + *, + kind: str = "transition", + atol: float = 1e-12, +) -> NDArray[np.float64]: + """Return J[i,j] = pi[i] K[i,j] - pi[j] K[j,i]. + + ``transition`` requires nonnegative row-stochastic K; ``rate`` requires + nonnegative off-diagonals, nonpositive diagonals and zero row sums. + pi is a normalized nonnegative vector and need not be stationary. + No input is normalized, clipped or reordered. Tolerance is absolute. + """ + tolerance = _tolerance(atol) + array = _square(kernel, "kernel") + pi = _real_array(probability, "probability", 1) + if ( + pi.shape != (len(array),) + or np.any(pi < 0) + or not np.isclose(pi.sum(), 1, rtol=0, atol=tolerance) + ): + raise ValueError( + "probability must be nonnegative, normalized and match kernel size" + ) + if kind == "transition": + valid = np.all(array >= 0) and np.allclose( + array.sum(axis=1), 1, rtol=0, atol=tolerance + ) + elif kind == "rate": + off_diagonal = array.copy() + np.fill_diagonal(off_diagonal, 0) + valid = ( + np.all(off_diagonal >= 0) + and np.all(np.diag(array) <= 0) + and np.allclose(array.sum(axis=1), 0, rtol=0, atol=tolerance) + ) + else: + raise ValueError("kind must be 'transition' or 'rate'") + if not valid: + raise ValueError(f"kernel does not satisfy the {kind} contract") + with np.errstate(over="ignore", invalid="ignore"): + flow = pi[:, None] * array + current = flow - flow.T + if not np.isfinite(current).all(): + raise ValueError("probability current exceeds finite floating-point range") + return current + + +def circulation_part( + kernel: ArrayLike, + probability: ArrayLike, + *, + kind: str = "transition", + atol: float = 1e-12, +) -> NDArray[np.float64]: + """Return the skew part of pi-weighted flow, exactly half the current.""" + return probability_current(kernel, probability, kind=kind, atol=atol) * 0.5 + + +def current_divergence( + current: ArrayLike, *, atol: float = 1e-12 +) -> NDArray[np.float64]: + """Return incoming node divergence div[j] = sum_i J[i,j]. + + A stationary Markov current has zero divergence. This function reports + the actual residual, rather than assuming stationarity from normalization. + """ + tolerance = _tolerance(atol) + array = _square(current, "current") + if not np.allclose(array * 0.5, -array.T * 0.5, rtol=0, atol=tolerance * 0.5): + raise ValueError("current must be skew-symmetric") + with np.errstate(over="ignore", invalid="ignore"): + divergence = array.sum(axis=0) + if not np.isfinite(divergence).all(): + raise ValueError("divergence exceeds finite floating-point range") + return divergence + + +def satisfies_detailed_balance( + kernel: ArrayLike, + probability: ArrayLike, + *, + kind: str = "transition", + atol: float = 1e-12, +) -> bool: + """Whether every pi-weighted pair has zero current within absolute atol.""" + tolerance = _tolerance(atol) + current = probability_current(kernel, probability, kind=kind, atol=tolerance) + return bool(np.all(np.abs(current) <= tolerance)) + + +@dataclass(frozen=True) +class GraphHodgeDecomposition: + """Euclidean projections and explicit harmonic remainder in edge order. + + ``solenoidal + harmonic`` is the divergence-free part. ``solenoidal`` + is the face-boundary projection; without faces it is zero and all cycle + flow is reported as harmonic. Potentials use the minimum-norm gauge. + """ + + gradient: NDArray[np.float64] + solenoidal: NDArray[np.float64] + harmonic: NDArray[np.float64] + node_potential: NDArray[np.float64] + face_potential: NDArray[np.float64] + + +def graph_hodge_decomposition( + incidence: ArrayLike, + flow: ArrayLike, + face_boundary: ArrayLike | None = None, + *, + atol: float = 1e-12, +) -> GraphHodgeDecomposition: + """Split real edge flow into gradient, face curl and harmonic remainder. + + B (nodes x edges) has exactly one -1 source and +1 target per column. + C (edges x faces), when supplied, must satisfy B C = 0. Projections use + least squares onto range(B.T) and range(C); the remainder lies in ker(B) + and ker(C.T), up to numerical error. Disconnected graphs are supported. + This is an unweighted finite graph/complex contract, not a PDE solver. + """ + tolerance = _tolerance(atol) + boundary = _real_array(incidence, "incidence", 2) + edge_flow = _real_array(flow, "flow", 1) + nodes, edges = boundary.shape + if nodes == 0 or edge_flow.shape != (edges,): + raise ValueError( + "flow must match incidence edge count and nodes must be nonempty" + ) + if not ( + np.isin(boundary, [-1, 0, 1]).all() + and np.all((boundary == -1).sum(axis=0) == 1) + and np.all((boundary == 1).sum(axis=0) == 1) + ): + raise ValueError("each incidence column must have one source and one target") + faces = ( + np.empty((edges, 0)) + if face_boundary is None + else _real_array(face_boundary, "face_boundary", 2) + ) + if faces.shape[0] != edges: + raise ValueError("face_boundary must match incidence edge count") + with np.errstate(over="ignore", invalid="ignore"): + chain = boundary @ faces + if not np.isfinite(chain).all() or not np.allclose( + chain, 0, rtol=0, atol=tolerance + ): + raise ValueError("face_boundary must satisfy incidence @ face_boundary = 0") + node_potential = np.linalg.lstsq(boundary.T, edge_flow, rcond=None)[0] + gradient = boundary.T @ node_potential + face_potential = np.linalg.lstsq(faces, edge_flow - gradient, rcond=None)[0] + solenoidal = faces @ face_potential + harmonic = edge_flow - gradient - solenoidal + if not all( + np.isfinite(part).all() + for part in (gradient, solenoidal, harmonic, node_potential, face_potential) + ): + raise ValueError("decomposition exceeds finite floating-point range") + return GraphHodgeDecomposition( + gradient, solenoidal, harmonic, node_potential, face_potential + ) diff --git a/GEO-INFER-MATH/tests/unit/AGENTS.md b/GEO-INFER-MATH/tests/unit/AGENTS.md index ca10a946d..f86a640b3 100644 --- a/GEO-INFER-MATH/tests/unit/AGENTS.md +++ b/GEO-INFER-MATH/tests/unit/AGENTS.md @@ -23,6 +23,7 @@ - `test_ai_convenience.py` - `test_bayes_convenience.py` +- `test_circulation.py` - `test_clustering.py` - `test_config_contract.py` - `test_convenience_api.py` diff --git a/GEO-INFER-MATH/tests/unit/README.md b/GEO-INFER-MATH/tests/unit/README.md index 281769a96..a3bdc52c6 100644 --- a/GEO-INFER-MATH/tests/unit/README.md +++ b/GEO-INFER-MATH/tests/unit/README.md @@ -6,6 +6,7 @@ Unit workspace within `GEO-INFER-MATH`. - `test_ai_convenience.py` - `test_bayes_convenience.py` +- `test_circulation.py` - `test_clustering.py` - `test_config_contract.py` - `test_convenience_api.py` diff --git a/GEO-INFER-MATH/tests/unit/test_circulation.py b/GEO-INFER-MATH/tests/unit/test_circulation.py new file mode 100644 index 000000000..0ff1ccbc4 --- /dev/null +++ b/GEO-INFER-MATH/tests/unit/test_circulation.py @@ -0,0 +1,143 @@ +"""Analytical witnesses for finite Markov currents and graph Hodge splits.""" + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal + +from geo_infer_math.core.circulation import ( + circulation_part, + current_divergence, + graph_hodge_decomposition, + probability_current, + satisfies_detailed_balance, + skew_part, +) + +TRIANGLE = np.array([[-1, 0, 1], [1, -1, 0], [0, 1, -1]]) + + +def test_skew_identities_and_input_ownership(): + matrix = np.array([[3.0, 7, -2], [1, 4, 9], [6, -3, 5]]) + original = matrix.copy() + skew = skew_part(matrix) + symmetric = matrix * 0.5 + matrix.T * 0.5 + assert_array_equal(skew, -skew.T) + assert np.trace(skew) == 0 + x = np.array([2.0, -1, 3]) + assert x @ skew @ x == 0 + assert_allclose(symmetric + skew, matrix) + assert_array_equal(matrix, original) + extreme = np.array([[0.0, 1e308], [-1e308, 0.0]]) + assert_array_equal(skew_part(extreme), extreme) + + +def test_nonuniform_detailed_balance_and_nonstationary_divergence(): + kernel = np.array([[0.75, 0.25], [0.5, 0.5]]) + pi = np.array([2 / 3, 1 / 3]) + assert satisfies_detailed_balance(kernel, pi) + assert_allclose(probability_current(kernel, pi), 0, atol=1e-15) + changed_pi = np.array([0.5, 0.5]) + current = probability_current(kernel, changed_pi) + assert not satisfies_detailed_balance(kernel, changed_pi) + assert_allclose(current_divergence(current), changed_pi @ kernel - changed_pi) + assert_allclose(circulation_part(kernel, changed_pi) * 2, current) + + +def test_stationary_cycle_is_divergence_free_but_not_reversible(): + kernel = np.array([[0.0, 1, 0], [0, 0, 1], [1, 0, 0]]) + pi = np.full(3, 1 / 3) + current = probability_current(kernel, pi) + assert_allclose(current_divergence(current), 0, atol=1e-15) + assert not satisfies_detailed_balance(kernel, pi) + assert_allclose(current, (kernel - kernel.T) / 3) + # Dropping circulation loses a nonzero current despite zero divergence. + assert np.linalg.norm(current) > 0.8 + permutation = [2, 0, 1] + assert_allclose( + probability_current(kernel[np.ix_(permutation, permutation)], pi[permutation]), + current[np.ix_(permutation, permutation)], + ) + + +def test_rate_kernel_current_and_stationarity(): + rate = np.array([[-2.0, 2], [1, -1]]) + pi = np.array([1 / 3, 2 / 3]) + assert satisfies_detailed_balance(rate, pi, kind="rate") + changed_pi = np.array([0.5, 0.5]) + current = probability_current(rate, changed_pi, kind="rate") + assert_allclose(current_divergence(current), changed_pi @ rate) + + +def test_hodge_triangle_with_and_without_face(): + flow = np.array([2.0, -1, 5]) + original = flow.copy() + closed = graph_hodge_decomposition(TRIANGLE, flow, np.ones((3, 1))) + assert_allclose(closed.solenoidal, [2, 2, 2]) + assert_allclose(closed.gradient, [0, -3, 3], atol=1e-14) + assert_allclose(closed.harmonic, 0, atol=1e-14) + assert_allclose(closed.gradient + closed.solenoidal + closed.harmonic, flow) + assert_allclose(TRIANGLE @ closed.solenoidal, 0, atol=1e-14) + assert abs(closed.gradient @ closed.solenoidal) < 1e-13 + open_complex = graph_hodge_decomposition(TRIANGLE, flow) + assert_allclose(open_complex.solenoidal, 0) + assert_allclose(open_complex.harmonic, [2, 2, 2]) + assert_allclose(TRIANGLE @ open_complex.harmonic, 0, atol=1e-14) + assert_array_equal(flow, original) + + +def test_disconnected_graph_gauge_and_edgeless_graph(): + incidence = np.array([[-1, 0], [1, 0], [0, -1], [0, 1], [0, 0]]) + result = graph_hodge_decomposition(incidence, [2, -3]) + assert_allclose(result.gradient, [2, -3]) + assert_allclose(result.harmonic, 0, atol=1e-14) + assert_allclose(result.node_potential, [-1, 1, 1.5, -1.5, 0]) + empty = graph_hodge_decomposition(np.empty((2, 0)), np.empty(0)) + assert empty.gradient.size == 0 + assert_array_equal(empty.node_potential, [0, 0]) + + +@pytest.mark.parametrize( + "matrix", [[], [[1, 2]], [[1, np.nan], [0, 1]], [[1j]], [[True]], [["1"]]] +) +def test_skew_rejects_invalid_matrix(matrix): + with pytest.raises(ValueError): + skew_part(matrix) + + +@pytest.mark.parametrize( + "kernel,pi,kind", + [ + ([[1, 0], [0, 1]], [1], "transition"), + ([[1, 0], [0, 1]], [0.5, -0.5], "transition"), + ([[1, 0], [0, 1]], [1, 1], "transition"), + ([[1, 0], [0, 1]], [np.nan, 0], "transition"), + ([[1, -1], [0, 1]], [0.5, 0.5], "transition"), + ([[0.5, 0], [0, 1]], [0.5, 0.5], "transition"), + ([[1, 0], [0, 1]], [0.5, 0.5], "unknown"), + ([[-1, -1], [1, -1]], [0.5, 0.5], "rate"), + ([[-1, 2], [1, -1]], [0.5, 0.5], "rate"), + ], +) +def test_kernel_contracts(kernel, pi, kind): + with pytest.raises(ValueError): + probability_current(kernel, pi, kind=kind) + + +@pytest.mark.parametrize("atol", [-1, np.inf, np.nan, True, "0", [0]]) +def test_invalid_tolerance(atol): + with pytest.raises(ValueError): + satisfies_detailed_balance(np.eye(2), [0.5, 0.5], atol=atol) + + +def test_invalid_current_and_graph_inputs(): + with pytest.raises(ValueError, match="skew-symmetric"): + current_divergence(np.eye(2)) + for incidence, flow, faces in [ + ([[1], [0]], [1], None), + (TRIANGLE, [1, 2], None), + (TRIANGLE, [1, 2, np.inf], None), + (TRIANGLE, [1, 2, 3], [[1], [0], [0]]), + (TRIANGLE, [1, 2, 3], [[1], [1]]), + ]: + with pytest.raises(ValueError): + graph_hodge_decomposition(incidence, flow, faces) diff --git a/GEO-INFER-TEST/doc_examples.json b/GEO-INFER-TEST/doc_examples.json index 4980e9f95..6f70bcf50 100644 --- a/GEO-INFER-TEST/doc_examples.json +++ b/GEO-INFER-TEST/doc_examples.json @@ -16,6 +16,7 @@ "GEO-INFER-INTRA/docs/guides/ENVIRONMENTAL_MONITORING_INTEGRATION.md", "GEO-INFER-INTRA/docs/guides/MODULE_INTEGRATION_GUIDE.md", "GEO-INFER-INTRA/docs/materiality/geo-infer-materiality.md", + "GEO-INFER-INTRA/docs/math/circulation.md", "GEO-INFER-INTRA/docs/modules/geo-infer-act.md", "GEO-INFER-INTRA/docs/modules/geo-infer-ag.md", "GEO-INFER-INTRA/docs/modules/geo-infer-agent.md", diff --git a/README.md b/README.md index 4a0015931..9451b4961 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ domain modeling, agent workflows, and reproducible repository validation in one | Metric | Value | | --- | ---: | | Modules | 45 | -| Python source files | 994 | -| Python test files | 786 | +| Python source files | 995 | +| Python test files | 787 | | Repository README.md files | 807 | | Repository AGENTS.md files | 804 | @@ -126,7 +126,7 @@ the exact reproducible exception list. | `GEO-INFER-IOT` | `geo_infer_iot` | 19 | 18 | | `GEO-INFER-LOG` | `geo_infer_log` | 21 | 20 | | `GEO-INFER-MARINE` | `geo_infer_marine` | 12 | 10 | -| `GEO-INFER-MATH` | `geo_infer_math` | 66 | 21 | +| `GEO-INFER-MATH` | `geo_infer_math` | 67 | 22 | | `GEO-INFER-METAGOV` | `geo_infer_metagov` | 21 | 16 | | `GEO-INFER-NORMS` | `geo_infer_norms` | 20 | 15 | | `GEO-INFER-OPS` | `geo_infer_ops` | 17 | 18 | diff --git a/TODO.md b/TODO.md index 6c81e775c..5d55128e2 100644 --- a/TODO.md +++ b/TODO.md @@ -28,6 +28,9 @@ next steps. ### 0.4.0 continuation — current scope (2026-10-08) +See the [issue reconciliation ledger](GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md) +for current evidence and explicitly unmet acceptance across all open issues. + The owner authorized repo-wide modernization, verification and an ordinary main push on 2026-10-08. Release tags, registry publication, deployment, licensed data, external hardware and account changes remain separately scoped. From c4c9f19c01e603d25daf7f0128452f54cc85ee4f Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 10:48:17 -0700 Subject: [PATCH 10/17] Refresh generated manuscript date after circulation contracts --- manuscript/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuscript/config.yaml b/manuscript/config.yaml index b475fad05..2bd67b192 100644 --- a/manuscript/config.yaml +++ b/manuscript/config.yaml @@ -10,7 +10,7 @@ paper: image: "output/figures/graphical_abstract.png" alt: "Graphical abstract: modules grouped into themes composing into geospatial and inference models, verified by an evidence loop into executable claims." version: "0.4.0" # generator-owned (PROJECT_VERSION) - date: "2026-10-08T10:32:29-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) + date: "2026-10-08T10:47:49-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) authors: - name: "Daniel Ari Friedman" From c65e4e6a9997c1bfe53a6a53b9c1d1d5e0c8954b Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 10:52:29 -0700 Subject: [PATCH 11/17] Skip unsaved cache pruning in CI reader jobs --- .github/workflows/ci.yml | 7 ++----- CHANGELOG.md | 3 ++- .../tests/unit/test_ci_workflow_contracts.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37e11b7fc..3adedf129 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,10 +218,6 @@ jobs: include-hidden-files: true retention-days: 14 - - name: Prune uv cache - if: always() - continue-on-error: true - run: uv cache prune --ci fast-contracts: # Early feedback is independent of the full, required category matrix. @@ -518,7 +514,8 @@ jobs: retention-days: 14 - name: Prune uv cache - if: always() + # Readers do not save a cache; prune only the selected cache writer. + if: always() && matrix.category == 'unit' continue-on-error: true run: uv cache prune --ci diff --git a/CHANGELOG.md b/CHANGELOG.md index b725c7260..1e08d5201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Repair SPM formula rejection, degenerate radial/Gaussian bases and the default polynomial basis expansion; document supported grammar and polynomial order. - Share locked CPU workspace installation and interpreter-specific uv package - caches across CI jobs, with one cache writer per interpreter. Cache parsed + caches across CI jobs, with one cache writer per interpreter. Prune only + those cache writers; readers skip cleanup of caches they never save. Cache parsed workflow fixtures while returning independent copies to each contract test. - Stop manuscript verification admission after interruption, retaining the interrupted attempt and explicit not-run groups. diff --git a/GEO-INFER-TEST/tests/unit/test_ci_workflow_contracts.py b/GEO-INFER-TEST/tests/unit/test_ci_workflow_contracts.py index 201f9479b..6dca2b79c 100644 --- a/GEO-INFER-TEST/tests/unit/test_ci_workflow_contracts.py +++ b/GEO-INFER-TEST/tests/unit/test_ci_workflow_contracts.py @@ -741,3 +741,18 @@ def test_cpu_cache_has_one_writer_per_interpreter_and_no_environment_reuse(): # Only the uv package download/build cache is reused; the composite # action still constructs and synchronizes each job's environment. assert ".venv" not in setup.get("cache-local-path", "") + + +def test_cpu_cache_readers_do_not_prune_unsaved_cache(): + jobs = _load("ci.yml")["jobs"] + for name in ("validate", "fast-contracts", "manuscript"): + assert not any( + "uv cache prune" in step.get("run", "") for step in jobs[name]["steps"] + ) + pruning = [ + step + for step in jobs["test"]["steps"] + if "uv cache prune" in step.get("run", "") + ] + assert len(pruning) == 1 + assert pruning[0]["if"] == "always() && matrix.category == 'unit'" From c78e06793afd3bcd81bb59e645a7ad6de7530592 Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 10:53:01 -0700 Subject: [PATCH 12/17] Refresh generated metadata after cache writer cleanup contract --- manuscript/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuscript/config.yaml b/manuscript/config.yaml index 2bd67b192..e6e5115a1 100644 --- a/manuscript/config.yaml +++ b/manuscript/config.yaml @@ -10,7 +10,7 @@ paper: image: "output/figures/graphical_abstract.png" alt: "Graphical abstract: modules grouped into themes composing into geospatial and inference models, verified by an evidence loop into executable claims." version: "0.4.0" # generator-owned (PROJECT_VERSION) - date: "2026-10-08T10:47:49-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) + date: "2026-10-08T10:52:29-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) authors: - name: "Daniel Ari Friedman" From 555eb7bbebcb654aa2cf25d3fcd931d37e31bfac Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 11:11:49 -0700 Subject: [PATCH 13/17] Reject non-pytest exit five as an empty selection --- CHANGELOG.md | 6 ++++ .../src/geo_infer_test/execution.py | 4 +-- .../tests/unit/test_run_unified_tests.py | 35 +++++++++++-------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e08d5201..288865ba9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 0.4.0 candidate preparation +- Make the non-pytest exit-code regression launch a real child and assert its + exit code, output, failure classification and complete receipt custody; + a metadata failure can no longer satisfy the test. Limit allowed-empty + exit-code handling and pytest diagnostics to actual pytest entry points; + ordinary commands returning code 5 remain failures even with allow_empty. + - Add MATH finite-real skew decomposition, transition/rate probability currents, detailed-balance checks and discrete graph Hodge projections with an explicit harmonic remainder. Analytical tests cover nonuniform stationary measures, diff --git a/GEO-INFER-TEST/src/geo_infer_test/execution.py b/GEO-INFER-TEST/src/geo_infer_test/execution.py index a734df20f..09d0308e0 100644 --- a/GEO-INFER-TEST/src/geo_infer_test/execution.py +++ b/GEO-INFER-TEST/src/geo_infer_test/execution.py @@ -641,7 +641,7 @@ def run_command( raise ValueError("terminal token/status mismatch") except (ValueError, IndexError) as exc: errors.append(f"missing or invalid terminal completion receipt: {exc}") - empty = allow_empty and rc == PYTEST_NO_TESTS_EXIT_CODE + empty = is_pytest and allow_empty and rc == PYTEST_NO_TESTS_EXIT_CODE errors.extend(junit_contract_errors(report_path, allow_empty=empty)) if is_pytest and promised_junit is None: errors.append("pytest command omitted its required JUnit report") @@ -661,7 +661,7 @@ def run_command( errors.extend(junit_selection_errors(report_path, selected)) except (OSError, ValueError, KeyError, TypeError) as exc: errors.append(f"missing or invalid test selection receipt: {exc}") - if rc == PYTEST_NO_TESTS_EXIT_CODE and not allow_empty: + if is_pytest and rc == PYTEST_NO_TESTS_EXIT_CODE and not allow_empty: errors.append("pytest collected no tests (exit code 5)") success = (rc == 0 or empty) and not errors if success: diff --git a/GEO-INFER-TEST/tests/unit/test_run_unified_tests.py b/GEO-INFER-TEST/tests/unit/test_run_unified_tests.py index 1aa78f7e9..1dfb49d64 100644 --- a/GEO-INFER-TEST/tests/unit/test_run_unified_tests.py +++ b/GEO-INFER-TEST/tests/unit/test_run_unified_tests.py @@ -162,23 +162,30 @@ def fake_run(command, **kwargs): def test_non_pytest_no_tests_exit_remains_failure(tmp_path, monkeypatch): runner = load_runner_module() - monkeypatch.setattr(runner, "PROJECT_ROOT", tmp_path) + monkeypatch.setattr(runner, "RESULTS_DIR", tmp_path / "results") - def fake_run(command, **kwargs): - return subprocess.CompletedProcess( - command, - runner.PYTEST_NO_TESTS_EXIT_CODE, - stdout="", - stderr="", + for allow_empty in (False, True): + result = runner.run_command( + [ + sys.executable, + "-c", + "print('non-pytest-executed', flush=True); raise SystemExit(5)", + ], + "non-pytest exit-code contract", + timeout=10, + cwd=tmp_path, + allow_empty=allow_empty, ) - monkeypatch.setattr(runner, "run_process", fake_run) - - result = runner.run_command( - ["python", "script.py"], "script", timeout=10, cwd=tmp_path - ) - - assert result.success is False + assert result.success is False + assert result.status == "FAIL" + assert result.returncode == runner.PYTEST_NO_TESTS_EXIT_CODE + assert result.stdout.strip() == "non-pytest-executed" + assert "pytest collected no tests" not in result.stderr + receipt = json.loads(Path(result.receipt).read_text()) + assert receipt["status"] == "FAIL" + assert receipt["returncode"] == 5 + assert receipt["custody_complete"] is True def test_write_summary_decodes_timeout_output_bytes(tmp_path, monkeypatch): From b3a5ed2f457bd7658172d017472533b3e094ea35 Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 11:12:23 -0700 Subject: [PATCH 14/17] Refresh manuscript source metadata after exit classification fix --- manuscript/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuscript/config.yaml b/manuscript/config.yaml index e6e5115a1..a03f94f8d 100644 --- a/manuscript/config.yaml +++ b/manuscript/config.yaml @@ -10,7 +10,7 @@ paper: image: "output/figures/graphical_abstract.png" alt: "Graphical abstract: modules grouped into themes composing into geospatial and inference models, verified by an evidence loop into executable claims." version: "0.4.0" # generator-owned (PROJECT_VERSION) - date: "2026-10-08T10:52:29-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) + date: "2026-10-08T11:11:49-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) authors: - name: "Daniel Ari Friedman" From ed6e4e5d058a6cdfb3f67e1301b4bcb039a9ac69 Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 12:54:47 -0700 Subject: [PATCH 15/17] Validate graph face directions independently of scale and refresh readiness records --- CHANGELOG.md | 6 ++ GEO-INFER-INTRA/docs/math/circulation.md | 4 +- .../docs/releases/0.4.0_readiness.md | 61 ++++++++++++------- .../releases/issue_reconciliation_2026_10.md | 16 ++--- .../src/geo_infer_math/core/circulation.py | 12 +++- GEO-INFER-MATH/tests/unit/test_circulation.py | 22 +++++++ TODO.md | 59 ++++++++---------- 7 files changed, 114 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 288865ba9..bccd0cdbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 0.4.0 candidate preparation +- Validate graph face-boundary directions independently of column magnitude; + tiny invalid faces cannot pass the chain check and produce divergent curl + projections. Analytical regressions cover tiny and large valid/invalid faces + and zero face columns. Refresh readiness records to distinguish verified + candidate evidence from historical failures and pending main publication. + - Make the non-pytest exit-code regression launch a real child and assert its exit code, output, failure classification and complete receipt custody; a metadata failure can no longer satisfy the test. Limit allowed-empty diff --git a/GEO-INFER-INTRA/docs/math/circulation.md b/GEO-INFER-INTRA/docs/math/circulation.md index 91d597d0c..98a969f85 100644 --- a/GEO-INFER-INTRA/docs/math/circulation.md +++ b/GEO-INFER-INTRA/docs/math/circulation.md @@ -45,7 +45,9 @@ assert np.allclose(parts.harmonic, 0) The unweighted discrete graph contract uses incidence `B` (nodes by edges), with one -1 source and +1 target per edge. Optional face boundary `C` (edges by faces) -must satisfy `B @ C = 0`. Least-squares projections return gradient in +must satisfy `B @ C = 0`. Validation applies `atol` after scaling each nonzero +face column to unit maximum magnitude, so tiny invalid directions are rejected +just like unit-size directions. Zero columns remain valid. Least-squares projections return gradient in `range(B.T)`, face-curl (`solenoidal`) in `range(C)` and the explicit harmonic remainder. The remainder lies in `ker(B)` and `ker(C.T)` up to numerical error. Node and face potentials use the minimum-norm gauge. Disconnected and edgeless diff --git a/GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md b/GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md index 9249f3b28..ae02c2e32 100644 --- a/GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md +++ b/GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md @@ -1,27 +1,44 @@ # GEO-INFER 0.4.0 candidate verification -## Current verified checkpoint — 2026-10-08 - -The starting main commit for the modernization pass is -`6781c75ed56de99790a35bd815d7b39054c44444`. Local and remote main match, -and the preparation branch points to the same commit. All 21 jobs in -[main CI](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734618009) -passed, including both Python versions' test categories, installed wheels, -paired interchange, manuscript and validation. The separate -[Windows PLACE workflow](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734617885) -also passed. Generated signposts are current at this checkpoint. - -Main integration has occurred; the preparation-only description and previous -remote-main baseline are superseded. The implementation retains sweep ancestry -at `cbe6b9c59975254487452d697c2ea961a19f2286`. Historical failed attempts remain -available in immutable receipts and Git history. Open readiness issue bodies -still describe older checkpoints; their reconciliation is separate from the -observed published commit and successful workflow conclusions. - -The current pass adds SPM input and numerical contracts and replaces obsolete -operational guidance. It requires new local and hosted evidence after the changes. -Starting-main successes do not establish the new revision's acceptance, an -independent hosted-artifact audit, physical GPU validation or a package release. +## Modernization verification — 2026-10-08 + +The modernization implementation is verified at clean candidate +`b3a5ed2f457bd7658172d017472533b3e094ea35`. All 21 jobs in +[candidate CI](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37822488566) +and all 29 PR checks passed, as did the separate whole-tree formatting dispatch. +The local canonical full-eleven producer passed, including all 45 unit suites +and 10,863 executed cases, integration, performance and H3. Separate ROOT +manuscript and native PDF layout profiles passed 144 and seven cases. + +The bounded hosted audit verified all 23 main-CI raw archive digests, canonical +receipt and report hashes, all 46 coverage dispositions, and both interpreters' +45 installed wheels, 45 base imports and 20 operation profiles. TEST measured +2,977/3,383 covered lines (87.9988%) against the unchanged 85% floor. ROOT has +no declared coverage floor. Supplementary Windows evidence verifies 52 positive +cases per interpreter and retains the intended optional-boundary negative +control separately. Hosted manuscript evidence covers the default seven groups; +it does not substitute for the local eleven-group producer. + +The local output snapshot contains 2,038 files with identical before/copy/after +hashes. This establishes captured-byte custody, not scientific review of every +existing output. GitNexus was rebuilt at the candidate; missing indexed symbols +remain unknown. Generated README/AGENTS signposts and executable docs pass. + +The starting main checkpoint was +`6781c75ed56de99790a35bd815d7b39054c44444`; main subsequently incorporated the +accepted GNN 4.1.0 pin at `c1027a9d538f4656ed854f2735874f0e0782686c`. +The candidate preserves that ancestry and sweep ancestor +`cbe6b9c59975254487452d697c2ea961a19f2286`. Historical failed attempts remain +available in immutable receipts and Git history. Prior issue descriptions are +historical checkpoints, not current verdicts. + +Main integration requires the independent candidate review, a fresh remote +ancestry check and actual same-SHA main-triggered evidence. Its outcome is +recorded in [PR #80](https://github.com/ActiveInferenceInstitute/GEO_INFER/pull/80) +and [issue #68](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/68). +Hardware, licensed data, historical failure attribution, configured advisor +access, whole-output scientific review and release publication retain their +separate unmet requirements in the [issue ledger](issue_reconciliation_2026_10.md). ## Stages and acceptance diff --git a/GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md b/GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md index bf9e6c192..9e10fc777 100644 --- a/GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md +++ b/GEO-INFER-INTRA/docs/releases/issue_reconciliation_2026_10.md @@ -2,8 +2,8 @@ The owner requested repository improvements, issue/PR reconciliation and ordinary integration into main. Closing an issue means its actual requirements are met; -archiving a deferred issue as not planned would preserve its requirements in -`TODO.md` and would not constitute technical acceptance. +the owner explicitly requested that unmet issues remain open. Deferral does +not constitute technical acceptance. The earlier readiness issues describe pre-integration checkpoints. Main `c1027a9d538f4656ed854f2735874f0e0782686c` incorporates the accepted GNN 4.1.0 @@ -13,14 +13,14 @@ independent review, local native acceptance or external prerequisites. | Issue | Implemented work / current evidence | Remaining acceptance | | --- | --- | --- | -| #58 | MATH `core.circulation`: skew part, transition/rate currents, detailed balance, incoming divergence, finite graph Hodge projections and explicit harmonic remainder; 28 analytical cases and 351 MATH unit cases pass locally. | Final candidate hosted checks and main integration. External formal-repository bridge edits are separate. | +| #58 | MATH `core.circulation`: skew part, transition/rate currents, detailed balance, incoming divergence, finite graph Hodge projections and explicit harmonic remainder; 28 analytical cases and 351 MATH unit cases pass locally. | Candidate hosted checks passed; main integration remains the closure gate. External formal-repository bridge edits are separate. | | #61 | Starting main and intermediate modernization candidates have green hosted gates; obsolete pre-integration status is superseded. | Required child acceptance and exact final main evidence. | | #62 | Phase-aware deadline and census diagnostics are implemented; hosted ordinary TEST succeeds on recent candidates. | Historical timeout attribution and fresh independent infrastructure review remain unresolved; recurrence-free runs alone are not causal proof. | -| #63 | Genuine TEST coverage is included in the unchanged 85% floor gate; recent hosted validation succeeds. | Audit the final candidate's complete 46 dispositions and retained behavior identities. | -| #64 | Analytical runtime changes are implemented, focused oracles and broad hosted matrix run. | All 45 strict imports now pass locally; full local suite acceptance still has retained timeout/census failures on this Mac. Fresh independent review is unavailable. | -| #65 | Two-interpreter installed-wheel and paired interchange lanes execute; GNN pin is the accepted 4.1.0 source, superseding the issue's older pin. | Complete final wheel/profile/interchange artifact audit and independent boundary review. | -| #66 | Executable docs, generated signposts, changelog and producer cancellation handling updated. | Local full-eleven producer acceptance, whole-output custody, final index and independent scientific/native render review remain distinct from hosted default-tier manuscript success. | -| #67 | Recent candidate hosted matrix and native Windows/portability/interchange lanes pass. | Final candidate workflows and every expected artifact audit; whole-tree format dispatch. | +| #63 | Genuine TEST coverage is included in the unchanged 85% floor gate; recent hosted validation succeeds. | All 46 candidate dispositions are audited; TEST measures 87.9988% against the unchanged 85% floor. Historical #62 causality remains separate. | +| #64 | Analytical runtime changes are implemented, focused oracles and broad hosted matrix run. | All 45 strict imports and the local full-eleven producer passed on b3a5ed2f4. Retained earlier failures remain diagnostic evidence. Independent review and final main evidence are separate gates. | +| #65 | Two-interpreter installed-wheel and paired interchange lanes execute; GNN pin is the accepted 4.1.0 source, superseding the issue's older pin. | Both candidate wheel/profile archives and paired evidence are audited. Independent optional-boundary review remains distinct. | +| #66 | Executable docs, generated signposts, changelog and producer cancellation handling updated. | Local full-eleven producer, 2,038-file byte-custody snapshot, current index and 144+7 ROOT/native profile cases pass at b3a5ed2f4. Independent scientific review of every existing output remains unclaimed. | +| #67 | Recent candidate hosted matrix and native Windows/portability/interchange lanes pass. | All 29 candidate checks and separate formatting pass; all 23 main-CI archives plus supplementary archives are audited. Fresh independent hosted review and same-SHA main evidence remain distinct. | | #68 | Concurrent GNN pin is incorporated through an ordinary merge; published ancestry preserved. | Final integration and same-SHA main workflows; independent GO is not claimed. | | #69 | CPU path and documented optional GPU boundary preserved. | Actual physical GPU execution, device/version evidence, cold/warm timings and memory require hardware. | | #70 | Existing captured layers/replay preserved. | Complete bioregion acquisition still requires a declared source, license, extent and resource budget. | diff --git a/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py b/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py index c651ea39a..4c4bad816 100644 --- a/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py +++ b/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py @@ -172,7 +172,9 @@ def graph_hodge_decomposition( """Split real edge flow into gradient, face curl and harmonic remainder. B (nodes x edges) has exactly one -1 source and +1 target per column. - C (edges x faces), when supplied, must satisfy B C = 0. Projections use + C (edges x faces), when supplied, must satisfy B C = 0 after each nonzero + column is scaled to unit maximum magnitude for the tolerance check. + Projections use least squares onto range(B.T) and range(C); the remainder lies in ker(B) and ker(C.T), up to numerical error. Disconnected graphs are supported. This is an unweighted finite graph/complex contract, not a PDE solver. @@ -198,8 +200,14 @@ def graph_hodge_decomposition( ) if faces.shape[0] != edges: raise ValueError("face_boundary must match incidence edge count") + # A tiny invalid column still spans an invalid direction: least squares + # cancels its magnitude. Validate directions rather than raw magnitudes. + scales = np.max(np.abs(faces), axis=0, initial=0) + normalized_faces = np.divide( + faces, scales, out=np.zeros_like(faces), where=scales != 0 + ) with np.errstate(over="ignore", invalid="ignore"): - chain = boundary @ faces + chain = boundary @ normalized_faces if not np.isfinite(chain).all() or not np.allclose( chain, 0, rtol=0, atol=tolerance ): diff --git a/GEO-INFER-MATH/tests/unit/test_circulation.py b/GEO-INFER-MATH/tests/unit/test_circulation.py index 0ff1ccbc4..d11ba8474 100644 --- a/GEO-INFER-MATH/tests/unit/test_circulation.py +++ b/GEO-INFER-MATH/tests/unit/test_circulation.py @@ -96,6 +96,28 @@ def test_disconnected_graph_gauge_and_edgeless_graph(): assert_array_equal(empty.node_potential, [0, 0]) +@pytest.mark.parametrize("scale", [1e-300, 1e-13, 1.0, 1e300]) +def test_face_chain_validation_is_independent_of_column_scale(scale): + with pytest.raises(ValueError, match="incidence @ face_boundary"): + graph_hodge_decomposition( + TRIANGLE, [2, 2, 2], scale * np.array([[1], [0], [0]]) + ) + faces = scale * np.ones((3, 1)) + result = graph_hodge_decomposition(TRIANGLE, [2, 2, 2], faces) + assert_allclose(result.solenoidal, [2, 2, 2], atol=1e-14) + assert_allclose(TRIANGLE @ result.solenoidal, 0, atol=1e-14) + assert_allclose(TRIANGLE @ result.harmonic, 0, atol=1e-14) + assert_allclose(result.gradient + result.solenoidal + result.harmonic, [2, 2, 2]) + assert_allclose(faces @ result.face_potential, result.solenoidal) + + +def test_zero_face_columns_are_preserved(): + result = graph_hodge_decomposition(TRIANGLE, [2, 2, 2], np.zeros((3, 2))) + assert_array_equal(result.face_potential, [0, 0]) + assert_array_equal(result.solenoidal, [0, 0, 0]) + assert_allclose(result.harmonic, [2, 2, 2]) + + @pytest.mark.parametrize( "matrix", [[], [[1, 2]], [[1, np.nan], [0, 1]], [[1j]], [[True]], [["1"]]] ) diff --git a/TODO.md b/TODO.md index 5d55128e2..eb66ffe11 100644 --- a/TODO.md +++ b/TODO.md @@ -35,25 +35,18 @@ The owner authorized repo-wide modernization, verification and an ordinary main push on 2026-10-08. Release tags, registry publication, deployment, licensed data, external hardware and account changes remain separately scoped. -The verified starting checkout is clean at -`6781c75ed56de99790a35bd815d7b39054c44444`, matching remote `main` and the -preparation branch. Main integration has occurred. The earlier statement that -main remained at `510f1008e698b45aedc4306d65caf898da6cda04` is superseded. -The preserved sweep ancestor is `cbe6b9c59975254487452d697c2ea961a19f2286`. -Historical failed attempts remain in immutable receipt directories and Git -history; they are not current acceptance verdicts. - -At this exact starting revision, [main CI](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734618009) -completed all 21 jobs successfully, including Python 3.11/3.12 unit, slow, -integration, system, performance and H3 categories, installed wheels, paired -interchange, manuscript and validation. [Native Windows PLACE](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37734617885), -[portability](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37736394591), -[formatting](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37736398107) -and [standalone interchange](https://github.com/ActiveInferenceInstitute/GEO_INFER/actions/runs/37736401172) -also passed. These are live workflow conclusions, not a fresh independent audit -of every uploaded artifact. The issue tracker has not yet been reconciled with -these results: required issues #61–68 remain open. Their original bodies describe -older checkpoints and must not override live Git and workflow evidence. +The starting-main checkpoint was +`6781c75ed56de99790a35bd815d7b39054c44444`. Main subsequently incorporated +GNN 4.1.0 at `c1027a9d538f4656ed854f2735874f0e0782686c`; modernization preserves +that ancestry and sweep ancestor `cbe6b9c59975254487452d697c2ea961a19f2286`. +The clean implementation candidate `b3a5ed2f457bd7658172d017472533b3e094ea35` +passed all 29 PR checks and separate formatting, the local full-eleven producer, +45 unit suites / 10,863 cases and separate 144+7 ROOT/native manuscript cases. +All 23 main-CI archives, 46 coverage dispositions and both installed-wheel +inventories are audited. The candidate index and generated signposts are current. +See [candidate evidence](GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md). +Historical failed attempts remain diagnostic evidence. Main integration and +same-SHA main evidence are tracked in #68; unmet issues remain open. The modernization pass replaces obsolete H3 setup guidance with supported APIs, corrects RNG documentation, and repairs SPM helper input rejection, silently @@ -62,40 +55,40 @@ polynomial basis's nonterminating expansion. Migration guidance records the new polynomial ordering and explicit supported formula grammar. CI now shares a locked CPU workspace sync and per-interpreter uv package cache, with one writer per key and isolated job environments. Workflow tests reuse parsed YAML through -independent copies; the full 35-contract selection passed. Working public +independent copies; the full 36-contract selection passed. Working public API names, security rejection tests, publisher URLs and hash-bound historical identities remain meaningful contracts rather than removal targets. -Final acceptance for this pass requires focused SPM numerical/rejection tests, -generated-signpost review, every strict repository gate and broad module -categories. New results remain bound to their measured source; the starting -revision's green hosted results cannot be relabeled as new-revision acceptance. +Focused SPM numerical/rejection tests, generated-signpost checks, strict repository +gates and broad module categories pass on the measured candidate. Independent +review, final publication and same-SHA main checks remain separate. Results stay +bound to their measured source; starting-main evidence is not new-revision proof. See [current candidate verification](GEO-INFER-INTRA/docs/releases/0.4.0_readiness.md) for the retained evidence boundaries and next acceptance steps. The [readiness tracker (#61)](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/61) links the complete next-work graph. Dependencies govern completion; preparation may overlap after a stable source freeze with bounded resources and explicit -source/output ownership. Resumed work starts with the two independent blockers -below; their historical failed evidence remains unchanged. +source/output ownership. The rows below retain acceptance requirements and historical failed evidence; +the issue ledger distinguishes completed candidate verification from unmet scope. | ID | Owning area / status | Complete bounded next step | Acceptance / dependencies | | --- | --- | --- | --- | | **R04-00** | Repository / acceptance reconciliation open | [#61: exact-candidate readiness](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/61) coordinates every required child and separate deferred scope. | One clean revision binds local, hosted, wheel, paired, security and manuscript evidence; successful required children precede main integration. | | **R04-01** | TEST / demonstrated timeout, cause unresolved | [#62: ordinary-validator timeout](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/62): inspect actual child/parent timing, ownership census, cleanup and receipts; repair only a demonstrated contract problem. | Real command outcomes, fresh nonzero JUnit where promised, validators without JUnit, invalid/empty selections, POSIX/Windows descendants and interruption; no automatic retries, suppressed errors or production-budget workaround. Fresh independent shared-infrastructure review. | -| **R04-02** | TEST / hosted coverage gate passed; final receipt reconciliation open | [#63: real runner behavior and 85% floor](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/63): review policy fix; add meaningful public runner/error/cancellation behavior coverage. | Preserve the 116 existing core behavior identities and 111 policy controls; fully passing whole TEST measurement with genuine raw coverage ≥85%. Complete all 46 final package/ROOT dispositions and inventory reconciliation. Depends on R04-01. | -| **R04-03** | Repository and connected modules / fresh modernization proof required | [#64: strict and analytical acceptance](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/64): freeze reviewed source and complete all declared local contracts/categories, real composition oracles, security and reproducibility; reduce measured fixture/setup cost without losing coverage. | UTC/DST/ns/step/gap/order/ownership/allocation contracts; real DATA→SPACE→TIME, BAYES/ACT permutations, seven-cell/pentagon transitions/posteriors/policy, IOT coordinate oracle, ART hostile input, OPS port cleanup and async lifecycle. Strict repository/package/docs/skills/test/model/logging/lint/format/workflow/secret gates. Depends on R04-01/R04-02. | -| **R04-04** | Packaging and GNN / starting-main hosted profiles passed | [#65: all wheels, optional boundaries and paired interchange](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/65). | 45 wheels, 45 base and 20 operation profiles outside checkout on 3.11/3.12; metadata/resources/extra parity, real signed DuckDB fast path; exact pinned GNN four-family/exporter/replay evidence. Stable candidate from R04-03. | -| **R04-05 / CODE-01** | Documentation, manuscript, index / current status reconciliation | [#66: executable docs and manuscript](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/66): finish truthful API/migration/configuration guidance, generated signposts, 0.4.0 parity, Git-derived metadata fixed point and source-current GitNexus. | Coordinator-only full eleven-group producer, independent whole-output custody/snapshot, real native PDF/figure inspection, generator drift checks and fresh ROOT inventory (currently 142+7). Retain producer manifest/inventory/variables/figure-registry JSON or state hosted upload limits. No old-output or default-seven-tier passing claim. Depends on stable R04-03 source. | -| **R04-06** | CI / starting-main workflows passed; artifact audit separate | [#67: complete hosted evidence](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/67): run declared workflows once on the reviewed candidate and audit actual receipts/artifact bytes. | Required unit/integration/performance/system/slow/H3 matrix on 3.11/3.12, native Windows and early contracts; actual profile/inventory reconciliation, all 46 floors, wheels/paired/ROOT/models/security and complete uploaded artifacts. Derive fresh counts; preserve failures. Depends on R04-01 through R04-05. | -| **R04-07** | Git/main / integrated; verify modernization publication | [#68: accepted main integration](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/68): when implementation is resumed and candidate GO exists, recheck remote/protection/ancestry, integrate ordinarily and verify exact published/main-triggered evidence. | No force-push, ancestry rewrite, stale-SHA acceptance or protection bypass; preserve rollback and unrelated work. Main SHA parity plus actual same-SHA hosted results. Depends on R04-03 through R04-06. | +| **R04-02** | TEST / candidate coverage and receipt reconciliation passed | [#63: real runner behavior and 85% floor](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/63): review policy fix; add meaningful public runner/error/cancellation behavior coverage. | Preserve the 116 existing core behavior identities and 111 policy controls; fully passing whole TEST measurement with genuine raw coverage ≥85%. Complete all 46 final package/ROOT dispositions and inventory reconciliation. Depends on R04-01. | +| **R04-03** | Repository and connected modules / candidate local and hosted gates passed | [#64: strict and analytical acceptance](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/64): freeze reviewed source and complete all declared local contracts/categories, real composition oracles, security and reproducibility; reduce measured fixture/setup cost without losing coverage. | UTC/DST/ns/step/gap/order/ownership/allocation contracts; real DATA→SPACE→TIME, BAYES/ACT permutations, seven-cell/pentagon transitions/posteriors/policy, IOT coordinate oracle, ART hostile input, OPS port cleanup and async lifecycle. Strict repository/package/docs/skills/test/model/logging/lint/format/workflow/secret gates. Depends on R04-01/R04-02. | +| **R04-04** | Packaging and GNN / candidate profiles and archive audit passed | [#65: all wheels, optional boundaries and paired interchange](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/65). | 45 wheels, 45 base and 20 operation profiles outside checkout on 3.11/3.12; metadata/resources/extra parity, real signed DuckDB fast path; exact pinned GNN four-family/exporter/replay evidence. Stable candidate from R04-03. | +| **R04-05 / CODE-01** | Documentation, manuscript, index / current status reconciliation | [#66: executable docs and manuscript](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/66): finish truthful API/migration/configuration guidance, generated signposts, 0.4.0 parity, Git-derived metadata fixed point and source-current GitNexus. | Coordinator-only full eleven-group producer, independent whole-output custody/snapshot, real native PDF/figure inspection, generator drift checks and fresh ROOT inventory (144+7 at b3a5ed2f4). Retain producer manifest/inventory/variables/figure-registry JSON or state hosted upload limits. No old-output or default-seven-tier passing claim. Depends on stable R04-03 source. | +| **R04-06** | CI / candidate workflows and author artifact audit passed | [#67: complete hosted evidence](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/67): run declared workflows once on the reviewed candidate and audit actual receipts/artifact bytes. | Required unit/integration/performance/system/slow/H3 matrix on 3.11/3.12, native Windows and early contracts; actual profile/inventory reconciliation, all 46 floors, wheels/paired/ROOT/models/security and complete uploaded artifacts. Derive fresh counts; preserve failures. Depends on R04-01 through R04-05. | +| **R04-07** | Git/main / final modernization publication pending | [#68: accepted main integration](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/68): when implementation is resumed and candidate GO exists, recheck remote/protection/ancestry, integrate ordinarily and verify exact published/main-triggered evidence. | No force-push, ancestry rewrite, stale-SHA acceptance or protection bypass; preserve rollback and unrelated work. Main SHA parity plus actual same-SHA hosted results. Depends on R04-03 through R04-06. | | **SPACE-01** | SPACE / owner-deferred GPU work | [#69: physical backend validation](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/69). GPU experiments are on hold; future work must be usable from a Mac laptop. | Establish a backend compatible with the Mac target and the declared precision contract before physical numerical parity, failure/chunk boundaries, memory and cold/warm timings. Preserve laptop CPU operation; H3 remains host CPU. No CPU-fallback hardware claim. | | **PLACE-V14** | PLACE / deferred licensed data | [#70: complete Cascadia boundary](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/70). | Authorized source/license/extent/resource budget, provenance/checksum/WGS84/stable identities and real renderer/integration; missing layer remains explicit and fail-closed. | | **PLACE-04** | PLACE / native Windows workflow passed; retained receipt audit separate | [#71: real Windows download worker](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/71). | Locked PLACE runtime, stalled-header/slow-drip deadlines, termination/pipe closure, batch failure and replay. Shared-process Windows CI does not close this separate item. | | **TEST-GNN-01** | SPACE/TIME/ACT / historical cause unresolved | [#72: PROJ SQLite I/O causality](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/72). | Bounded evidence-driven reproduction with version/import/file state; respect prior negative lock probes and distinguish non-recurrence from explanation. Do not suppress CRS tests. | | **TEST-04** | TEST / deferred configured-advisor review | [#73: advisor review](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/73). | Human-owned account/authentication decisions; actual configured-service review bound to the candidate when available. No auth/key/model/billing/hook changes or review claim from service errors. | | **REL-04 / REL-01** | Release / separate future authorization | [#74: 0.4.0 release decision and execution](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/74). | Explicit tag/release/attachment/registry authorization after exact-main readiness; verify destination bytes and retain rollback. Main preparation does not authorize publication or deployment. | -| **ACT-MATH-58** | ACT/MATH / separate future feature | [Existing #58: reviewed skew/circulation/Helmholtz constructs](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/58). | Scope actual numerical APIs, transition/rate/current conventions and independent mathematical fixtures before notation-bridge symbol binding. Current diagnostics/internal symmetrization do not establish those APIs. Not a required 0.4.0 dependency. | +| **ACT-MATH-58** | MATH / implemented; final integration pending | [Existing #58: reviewed skew/circulation/Helmholtz constructs](https://github.com/ActiveInferenceInstitute/GEO_INFER/issues/58). | Finite skew/current/circulation/divergence and discrete graph Hodge APIs are implemented with 28 analytical cases and an executable guide. Close after verified main integration; external notation-bridge edits and formal proof acceptance are separate. Not a required 0.4.0 dependency. | Implemented preparation changes remain recorded in [CHANGELOG.md](CHANGELOG.md); do not reopen completed work without a concrete source/behavior finding. From abf136325211694bb79645d01ddc7060ce1cf84f Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 12:56:06 -0700 Subject: [PATCH 16/17] Validate and project the same numerical graph face space --- CHANGELOG.md | 4 +-- GEO-INFER-INTRA/docs/math/circulation.md | 7 +++-- .../src/geo_infer_math/core/circulation.py | 30 ++++++++++++------- GEO-INFER-MATH/tests/unit/test_circulation.py | 18 +++++++++++ 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bccd0cdbb..283e438fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 0.4.0 candidate preparation -- Validate graph face-boundary directions independently of column magnitude; +- Validate an orthonormal basis of the graph face-boundary numerical span; tiny invalid faces cannot pass the chain check and produce divergent curl projections. Analytical regressions cover tiny and large valid/invalid faces - and zero face columns. Refresh readiness records to distinguish verified + zero and dependent valid columns, and near-dependent invalid columns. Refresh readiness records to distinguish verified candidate evidence from historical failures and pending main publication. - Make the non-pytest exit-code regression launch a real child and assert its diff --git a/GEO-INFER-INTRA/docs/math/circulation.md b/GEO-INFER-INTRA/docs/math/circulation.md index 98a969f85..ffa0e99dd 100644 --- a/GEO-INFER-INTRA/docs/math/circulation.md +++ b/GEO-INFER-INTRA/docs/math/circulation.md @@ -45,9 +45,10 @@ assert np.allclose(parts.harmonic, 0) The unweighted discrete graph contract uses incidence `B` (nodes by edges), with one -1 source and +1 target per edge. Optional face boundary `C` (edges by faces) -must satisfy `B @ C = 0`. Validation applies `atol` after scaling each nonzero -face column to unit maximum magnitude, so tiny invalid directions are rejected -just like unit-size directions. Zero columns remain valid. Least-squares projections return gradient in +must satisfy `B @ C = 0`. Validation applies `atol` to an orthonormal basis of the numerical face span, +using the default least-squares rank cutoff. Uniform scaling preserves directions; +nearly dependent columns cannot hide a non-cycle direction. Zero columns remain +valid. The same basis computes projections and minimum-norm face potentials. Least-squares projections return gradient in `range(B.T)`, face-curl (`solenoidal`) in `range(C)` and the explicit harmonic remainder. The remainder lies in `ker(B)` and `ker(C.T)` up to numerical error. Node and face potentials use the minimum-norm gauge. Disconnected and edgeless diff --git a/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py b/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py index 4c4bad816..eab836bd9 100644 --- a/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py +++ b/GEO-INFER-MATH/src/geo_infer_math/core/circulation.py @@ -172,8 +172,8 @@ def graph_hodge_decomposition( """Split real edge flow into gradient, face curl and harmonic remainder. B (nodes x edges) has exactly one -1 source and +1 target per column. - C (edges x faces), when supplied, must satisfy B C = 0 after each nonzero - column is scaled to unit maximum magnitude for the tolerance check. + C (edges x faces), when supplied, must have a numerical column space in + ker(B), checked on its orthonormal basis using atol. Projections use least squares onto range(B.T) and range(C); the remainder lies in ker(B) and ker(C.T), up to numerical error. Disconnected graphs are supported. @@ -200,22 +200,32 @@ def graph_hodge_decomposition( ) if faces.shape[0] != edges: raise ValueError("face_boundary must match incidence edge count") - # A tiny invalid column still spans an invalid direction: least squares - # cancels its magnitude. Validate directions rather than raw magnitudes. - scales = np.max(np.abs(faces), axis=0, initial=0) - normalized_faces = np.divide( - faces, scales, out=np.zeros_like(faces), where=scales != 0 + # Validate the span, not individual columns: nearly dependent columns + # can amplify a tiny invalid residual into a non-cycle direction. + face_scale = np.max(np.abs(faces), initial=0) + scaled_faces = np.divide( + faces, face_scale, out=np.zeros_like(faces), where=face_scale != 0 ) + left, singular, right = np.linalg.svd(scaled_faces, full_matrices=False) + cutoff = ( + np.finfo(float).eps * max(faces.shape) * singular[0] if singular.size else 0 + ) + retained = singular > cutoff + face_basis = left[:, retained] with np.errstate(over="ignore", invalid="ignore"): - chain = boundary @ normalized_faces + chain = boundary @ face_basis if not np.isfinite(chain).all() or not np.allclose( chain, 0, rtol=0, atol=tolerance ): raise ValueError("face_boundary must satisfy incidence @ face_boundary = 0") node_potential = np.linalg.lstsq(boundary.T, edge_flow, rcond=None)[0] gradient = boundary.T @ node_potential - face_potential = np.linalg.lstsq(faces, edge_flow - gradient, rcond=None)[0] - solenoidal = faces @ face_potential + coefficients = face_basis.T @ (edge_flow - gradient) + solenoidal = face_basis @ coefficients + with np.errstate(over="ignore", invalid="ignore", divide="ignore"): + face_potential = right[retained].T @ (coefficients / singular[retained]) + if face_scale: + face_potential = face_potential / face_scale harmonic = edge_flow - gradient - solenoidal if not all( np.isfinite(part).all() diff --git a/GEO-INFER-MATH/tests/unit/test_circulation.py b/GEO-INFER-MATH/tests/unit/test_circulation.py index d11ba8474..4784882bc 100644 --- a/GEO-INFER-MATH/tests/unit/test_circulation.py +++ b/GEO-INFER-MATH/tests/unit/test_circulation.py @@ -118,6 +118,24 @@ def test_zero_face_columns_are_preserved(): assert_allclose(result.harmonic, [2, 2, 2]) +def test_nearly_dependent_faces_cannot_hide_noncycle_direction(): + boundary = np.array( + [[-1, 0, 1, -1, 0], [1, -1, 0, 0, 0], [0, 1, -1, 0, 1], [0, 0, 0, 1, -1]] + ) + cycle = np.array([1.0, 1.0, 1.0, 0.0, 0.0]) + faces = np.column_stack((cycle, cycle + 1e-13 * np.array([0, 0, 0, 1, 0]))) + with pytest.raises(ValueError, match="incidence @ face_boundary"): + graph_hodge_decomposition(boundary, [0, 0, 1, 1, 1], faces) + + +def test_dependent_valid_faces_preserve_minimum_norm_potential(): + faces = np.column_stack((np.ones(3), 2 * np.ones(3))) + result = graph_hodge_decomposition(TRIANGLE, [2, 2, 2], faces) + assert_allclose(result.face_potential, [0.4, 0.8]) + assert_allclose(faces @ result.face_potential, [2, 2, 2]) + assert_allclose(TRIANGLE @ result.solenoidal, 0, atol=1e-14) + + @pytest.mark.parametrize( "matrix", [[], [[1, 2]], [[1, np.nan], [0, 1]], [[1j]], [[True]], [["1"]]] ) From 42d2964b4a3919f3865b2b41a69acbe100b97c7c Mon Sep 17 00:00:00 2001 From: docxology Date: Thu, 8 Oct 2026 12:56:17 -0700 Subject: [PATCH 17/17] Refresh canonical manuscript metadata after numerical review fixes --- manuscript/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuscript/config.yaml b/manuscript/config.yaml index a03f94f8d..df9b91edb 100644 --- a/manuscript/config.yaml +++ b/manuscript/config.yaml @@ -10,7 +10,7 @@ paper: image: "output/figures/graphical_abstract.png" alt: "Graphical abstract: modules grouped into themes composing into geospatial and inference models, verified by an evidence loop into executable claims." version: "0.4.0" # generator-owned (PROJECT_VERSION) - date: "2026-10-08T11:11:49-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) + date: "2026-10-08T12:56:06-07:00" # generator-owned (MANUSCRIPT_SOURCE_DATE) authors: - name: "Daniel Ari Friedman"