hypertopos 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hypertopos-0.2.1 → hypertopos-0.2.2}/CHANGELOG.md +18 -7
- {hypertopos-0.2.1 → hypertopos-0.2.2}/PKG-INFO +1 -1
- {hypertopos-0.2.1 → hypertopos-0.2.2}/README.md +3 -3
- {hypertopos-0.2.1 → hypertopos-0.2.2}/ROADMAP.md +15 -3
- {hypertopos-0.2.1 → hypertopos-0.2.2}/SECURITY.md +1 -1
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/api-reference.md +8 -8
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/architecture.md +5 -1
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/concepts.md +1 -1
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/__init__.py +1 -1
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/navigation/navigator.py +109 -28
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/navigation/scanner.py +14 -8
- {hypertopos-0.2.1 → hypertopos-0.2.2}/pyproject.toml +1 -1
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_detection.py +25 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_geometric_path.py +100 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_scanner.py +12 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/.github/workflows/publish-pypi.yml +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/.gitignore +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/LICENSE.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/README.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/LICENSE +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/README.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/README.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/SUMMARY.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_accounts.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_cpty_banks.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_districts.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_loan_accounts.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_operations.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_orders.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_transactions.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_tx_types.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/sphere.yaml +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/README.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/_layer2_cross_eval.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/_layer3_cross_eval.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_accounts.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_currencies.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_payment_formats.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_transactions.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/sphere.yaml +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/layer1_passive_scan.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/layer2_xgboost_sweep.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/layer3_typology_filter.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_accounts.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_currencies.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_payment_formats.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_transactions.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/sphere.yaml +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/passive-scan-calibration.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/README.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/SUMMARY.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-c-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-c-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-c-report.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-settings.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/sphere/prepare_trips.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/sphere/sphere.yaml +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmarks.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/configuration.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/data-format.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/build-pipeline.svg +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/core-objects.svg +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/cutting-plane.svg +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/directory-layout.svg +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/hypertopos-overview.svg +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/model-lifecycle.svg +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/navigation-primitives.svg +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/three-scales.svg +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/quickstart.md +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/__init__.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/_stats.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/_writer.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/builder.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/derived.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/mapping.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/__init__.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/__main__.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/build.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/info.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/main.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/schema.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/sources.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/__init__.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/aggregation.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/calibration.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/chains.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/composition.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/datafusion_agg.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/forecast.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/geometry.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/invalidation.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/investigation.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/subprocess_agg.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/model/__init__.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/model/manifest.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/model/objects.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/model/sphere.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/navigation/__init__.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/sphere.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/__init__.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/_schemas.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/cache.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/reader.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/writer.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/utils/__init__.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/utils/arrow.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/.gitkeep +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/conftest.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/fixtures/generate_fixtures.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_aggregation_engine.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_arrow_utils.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_builder.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_builder_alias.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_builder_derived.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_builder_edges.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_cache.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_calibration.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_chains.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_cli.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_cli_alias.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_cli_sources.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_compaction.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_composition.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_continuous_event_dims.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_data_quality_prop.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_datafusion_agg.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_edge_table.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_engine.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_forecast.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_fts.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_geometry_stats.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_hybrid_search.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_incremental.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_invalidation.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_investigation.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_mapping.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_model.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_mvcc_session.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_navigation.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_navigator_alerts.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_observability.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_precision_stack.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_reputation.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_row_id_cache.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_sphere.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_sphere_overview_perf.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_stats_guard.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_storage.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_streaming_reads.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_subprocess_agg.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_temporal_centroids.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_trajectory_ann.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_witness_cohort.py +0 -0
- {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_witness_set.py +0 -0
|
@@ -5,6 +5,23 @@ All notable changes to hypertopos will be documented in this file.
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.2] — 2026-04-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
**Temporal as-of reconstruction across edge-table graph primitives**
|
|
13
|
+
- `contagion_score()`, `contagion_score_batch()` — optional keyword-only `timestamp_cutoff: float | None` parameter. When set, only edges with `timestamp <= timestamp_cutoff` are considered. Batch forwards the cutoff to every per-entity call.
|
|
14
|
+
- `entity_flow()` — same `timestamp_cutoff` parameter; both outgoing and incoming edge reads honor the cutoff, so net flow reflects the as-of graph state.
|
|
15
|
+
- `degree_velocity()` — same parameter; buckets derive from the filtered edge set so the last bucket endpoint is naturally `<= timestamp_cutoff`.
|
|
16
|
+
- `propagate_influence()` — same parameter; threaded through the BFS `_expand_neighbors` closure so expansion follows only edges with `timestamp <= cutoff`.
|
|
17
|
+
- `find_counterparties()` — same parameter on the edge-table fast path via `_find_counterparties_via_edges`. The points-scan fallback has no timestamp column and **raises `GDSNavigationError`** when `timestamp_cutoff` is supplied — fail loudly instead of silently returning unfiltered results.
|
|
18
|
+
|
|
19
|
+
The semantic mirrors the existing `WitnessCohortConfig.timestamp_cutoff` from 0.2.1: edges with `timestamp <= timestamp_cutoff` are included. Enables agents to reconstruct contagion, flow, connection velocity, and influence propagation state at a prior point in time — useful for incident forensics ("what did this neighborhood look like on the day the alert fired?"), retroactive detection validation, and historical-snapshot comparisons.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `detect_cross_pattern_discrepancy` no longer triggers full edge-table scans through `PassiveScanner.auto_discover`. The scanner gains an `include_graph: bool = True` keyword-only parameter; `detect_cross_pattern_discrepancy` calls it with `include_graph=False` because graph contagion plays no role in the downstream geometry-disagreement check. Eliminates the dominant latency regression on multi-pattern spheres with edge tables — the discrepancy detector previously paid one full edge-table read per event pattern with no signal benefit. Every other `auto_discover` caller (`composite_risk`, explicit scanner use) stays at the graph-enabled default.
|
|
24
|
+
|
|
8
25
|
## [0.2.1] — 2026-04-11
|
|
9
26
|
|
|
10
27
|
### Added
|
|
@@ -15,17 +32,11 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
15
32
|
- Excludes already-connected entities via BTREE edge lookup; bidirectional check by default; `timestamp_cutoff` for as-of evaluation in temporal hold-out
|
|
16
33
|
- Auto-resolves the event pattern with edge table covering an anchor's entity line; explicit `edge_pattern_id` override is validated
|
|
17
34
|
- Trajectory branch decided once per call (not per candidate) — when ref has trajectory, missing candidates get neutral 0.5 instead of mixed renormalization
|
|
18
|
-
- Batch trajectory load via single Lance scan instead of per-candidate (~11× speedup
|
|
35
|
+
- Batch trajectory load via single Lance scan instead of per-candidate (~11× speedup over the per-candidate path)
|
|
19
36
|
- `WitnessCohortConfig` and `WitnessCohortWeights` dataclasses group all tunable parameters; navigator API takes a single `config=` keyword
|
|
20
37
|
- `CohortMember` and `WitnessCohortResult` frozen dataclasses with per-component scores, exclusion counts, and reproducibility metadata
|
|
21
38
|
- `GDSEngine.witness_jaccard()`, `GDSEngine.trajectory_cosine()`, `GDSEngine.composite_link_score()` — pure scoring helpers exposed for reuse
|
|
22
39
|
|
|
23
|
-
**Empirical validation on AML HI-small** (5M edges, 6357 labeled launderers):
|
|
24
|
-
- Co-laundering precision@10: **25.3%** (vs 1.2% base rate = **20.5× lift**)
|
|
25
|
-
- 2.6× improvement over `find_similar_entities + is_anomaly` baseline (which scored 6.5%)
|
|
26
|
-
- Top-10 overlap with that baseline: 15.5% (function returns substantively different entities, not the same ranking)
|
|
27
|
-
- Temporal hold-out recall@10: **0.0%** — confirms the function does NOT forecast future edges; it surfaces existing geometric peers
|
|
28
|
-
|
|
29
40
|
## [0.2.0] — 2026-04-10
|
|
30
41
|
|
|
31
42
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypertopos
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Navigate your data like space.
|
|
5
5
|
Project-URL: Homepage, https://github.com/hypertopos/hypertopos-py
|
|
6
6
|
Project-URL: Documentation, https://github.com/hypertopos/hypertopos-py/tree/main/docs
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://arrow.apache.org/docs/python/)
|
|
9
9
|
[](https://github.com/lancedb/lancedb)
|
|
10
10
|
[](https://modelcontextprotocol.io)
|
|
11
|
-
[](pyproject.toml)
|
|
12
12
|
|
|
13
13
|
**hypertopos** transforms relational data into navigable geometric space.
|
|
14
14
|
|
|
@@ -38,7 +38,7 @@ Navigation primitives turn this space into a stateful, step-by-step workspace, w
|
|
|
38
38
|
|
|
39
39
|
- **Geometry, not queries** — entities live in a population-calibrated coordinate space (μ, σ, θ). Position tells you what's typical. Distance reveals what's unusual, what's similar, and where clusters form.
|
|
40
40
|
- **Navigate, don't search** — twelve primitives (π1–π12) let agents walk lines, jump across relationships, cluster populations, compare groups, find hubs, and track drift. Stateful, composable, geometric.
|
|
41
|
-
- **Graph meets geometry** — edge tables give runtime graph traversal with geometric path scoring. Find paths scored by witness overlap and anomaly propagation — not just hop count. Lazy chain discovery without build-time extraction. Surface witness cohorts (`find_witness_cohort`) — entities sharing the target's anomaly signature, ranked by delta similarity, witness overlap, trajectory alignment, and graded anomaly bonus, with already-connected entities filtered out. Investigative peer ranking
|
|
41
|
+
- **Graph meets geometry** — edge tables give runtime graph traversal with geometric path scoring. Find paths scored by witness overlap and anomaly propagation — not just hop count. Lazy chain discovery without build-time extraction. Surface witness cohorts (`find_witness_cohort`) — entities sharing the target's anomaly signature, ranked by delta similarity, witness overlap, trajectory alignment, and graded anomaly bonus, with already-connected entities filtered out. Investigative peer ranking, not edge forecasting.
|
|
42
42
|
- **Time is built in** — polygons accumulate into solids. Trajectory similarity, regime change detection, and drift analysis are first-class — not bolted on.
|
|
43
43
|
- **Agents first** — designed for MCP tool-calling. Smart detection recipes, investigation workflows. The sphere is the agent's workspace.
|
|
44
44
|
- **Zero training** — no ML models, no labels, no training pipeline. Population statistics and empirical thresholds. Deploy in minutes.
|
|
@@ -56,7 +56,7 @@ Navigation primitives turn this space into a stateful, step-by-step workspace, w
|
|
|
56
56
|
| **Navigation** | 12 stateful primitives | Query-based | KNN search | N/A |
|
|
57
57
|
| **Multi-hop** | Edge table (runtime graph + lazy chains) + pre-computed chain geometry | Native (Cypher, ad-hoc) | No | No |
|
|
58
58
|
| **Anomaly propagation** | Contagion scoring + influence BFS in delta-space | No | No | Local neighborhood only |
|
|
59
|
-
| **Witness cohort discovery** | Witness overlap + delta + trajectory + edge absence
|
|
59
|
+
| **Witness cohort discovery** | Witness overlap + delta + trajectory + edge absence | Topological only (Adamic-Adar, common neighbors) | KNN, no graph awareness | Learned embeddings (training required) |
|
|
60
60
|
| **Agent-native (MCP)** | Yes | No | No | No |
|
|
61
61
|
| **Explains why** | Witness sets, dimension ranking | Centrality scores | Nearest neighbors | Anomaly scores |
|
|
62
62
|
|
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
> Planned direction. Priorities may change based on feedback.
|
|
4
4
|
|
|
5
|
-
## Current: 0.2.
|
|
5
|
+
## Current: 0.2.2
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
As-of graph reconstruction across edge-table graph primitives + latency fix on `detect_cross_pattern_discrepancy`. See CHANGELOG.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 0.2.1
|
|
12
|
+
|
|
13
|
+
Witness cohort discovery — investigative peer ranking. See CHANGELOG.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 0.2.0
|
|
18
|
+
|
|
19
|
+
Graph meets geometry — edge table, runtime traversal, contagion/influence, +11 navigator functions. See CHANGELOG.
|
|
8
20
|
|
|
9
21
|
---
|
|
10
22
|
|
|
11
23
|
## 0.1.0
|
|
12
24
|
|
|
13
|
-
|
|
25
|
+
First public release — full GDS stack, π1–π12, builder, MCP server, validation suite. See CHANGELOG.
|
|
14
26
|
|
|
15
27
|
---
|
|
16
28
|
|
|
@@ -124,15 +124,15 @@ with sphere.session("agent-1") as session:
|
|
|
124
124
|
|--------|-------------|
|
|
125
125
|
| `find_geometric_path(from_key, to_key, pattern_id, max_depth=5, beam_width=10, scoring="geometric")` | Beam search for paths between two entities via the edge table, scored by geometric coherence. Scoring modes: `"geometric"` (witness overlap + delta alignment + anomaly preservation), `"amount"` (geometric score modulated by log(transaction amount)), `"anomaly"` (prefer paths through anomalous entities), `"shortest"` (plain BFS, no geometric scoring). Returns best paths with per-hop scores |
|
|
126
126
|
| `discover_chains(primary_key, pattern_id, time_window_hours=168, max_hops=10, min_hops=2, max_chains=100, direction="forward")` | Runtime temporal BFS on the edge table to discover entity chains from a starting point. Unlike `find_chains_for_entity()` which queries pre-computed chains, this performs live traversal -- works without build-time chain extraction |
|
|
127
|
-
| `entity_flow(primary_key, pattern_id, top_n=20)` | Net flow analysis per counterparty via edge table. Two edge lookups (outgoing + incoming), sum amounts, compute per-counterparty net flow. Returns outgoing/incoming totals, net_flow, flow_direction, counterparties sorted by abs(net_flow) |
|
|
128
|
-
| `contagion_score(primary_key, pattern_id)` | Score how many of an entity's counterparties are anomalous via edge table + geometry check. Returns score (0.0–1.0), total/anomalous counterparty counts |
|
|
129
|
-
| `contagion_score_batch(primary_keys, pattern_id, max_keys=200)` | Batch contagion scoring for multiple entities. Returns per-entity scores plus summary (mean, max, high_contagion_count) |
|
|
130
|
-
| `degree_velocity(primary_key, pattern_id, n_buckets=4)` | Temporal connection velocity — buckets edges by timestamp, counts unique counterparties per bucket. Velocity = last_bucket_degree / first_bucket_degree. Returns buckets with out/in degree, velocity metrics |
|
|
127
|
+
| `entity_flow(primary_key, pattern_id, top_n=20, *, timestamp_cutoff=None)` | Net flow analysis per counterparty via edge table. Two edge lookups (outgoing + incoming), sum amounts, compute per-counterparty net flow. `timestamp_cutoff` (Unix seconds) restricts to edges with `timestamp <= cutoff`. Returns outgoing/incoming totals, net_flow, flow_direction, counterparties sorted by abs(net_flow) |
|
|
128
|
+
| `contagion_score(primary_key, pattern_id, *, timestamp_cutoff=None)` | Score how many of an entity's counterparties are anomalous via edge table + geometry check. `timestamp_cutoff` enables as-of contagion reconstruction. Returns score (0.0–1.0), total/anomalous counterparty counts |
|
|
129
|
+
| `contagion_score_batch(primary_keys, pattern_id, max_keys=200, *, timestamp_cutoff=None)` | Batch contagion scoring for multiple entities — forwards `timestamp_cutoff` to each per-entity call. Returns per-entity scores plus summary (mean, max, high_contagion_count) |
|
|
130
|
+
| `degree_velocity(primary_key, pattern_id, n_buckets=4, *, timestamp_cutoff=None)` | Temporal connection velocity — buckets edges by timestamp, counts unique counterparties per bucket. Velocity = last_bucket_degree / first_bucket_degree. `timestamp_cutoff` clamps the last bucket endpoint by filtering edges at the read level. Returns buckets with out/in degree, velocity metrics |
|
|
131
131
|
| `investigation_coverage(primary_key, pattern_id, explored_keys)` | Agent guidance: how much of an entity's edge neighborhood has been explored. Splits counterparties into explored/unexplored, batch anomaly check on unexplored |
|
|
132
|
-
| `propagate_influence(seed_keys, pattern_id, max_depth=3, decay=0.7, min_threshold=0.001, max_affected=10_000)` | BFS influence propagation from seed entities with geometric decay and tx_count weighting. At each hop: influence = parent_score * decay * geometric_coherence * tx_weight. Returns affected_entities with tx_count per neighbor |
|
|
132
|
+
| `propagate_influence(seed_keys, pattern_id, max_depth=3, decay=0.7, min_threshold=0.001, max_affected=10_000, *, timestamp_cutoff=None)` | BFS influence propagation from seed entities with geometric decay and tx_count weighting. At each hop: influence = parent_score * decay * geometric_coherence * tx_weight. `timestamp_cutoff` restricts BFS expansion to edges with `timestamp <= cutoff`. Returns affected_entities with tx_count per neighbor |
|
|
133
133
|
| `cluster_bridges(pattern_id, n_clusters=5, top_n_bridges=10)` | Find entities bridging geometric clusters via edge table. Runs π8 clustering then identifies cross-cluster edges and bridge entities |
|
|
134
134
|
| `anomalous_edges(from_key, to_key, pattern_id, top_n=10)` | Find edges between two entities enriched with event-level geometry (delta_norm, is_anomaly). Unlike path tools which score entities (anchor), this scores individual transactions (event geometry) |
|
|
135
|
-
| `find_witness_cohort(primary_key, pattern_id, top_n=10, *, config=None, edge_pattern_id=None)` | Rank entities that share the target's witness signature. Investigative peer ranking — NOT edge forecasting. Combines four signals: `exp(-distance/theta)` delta similarity, witness Jaccard overlap, trajectory cosine alignment (optional), and graded anomaly bonus from `delta_rank_pct`. Excludes entities already connected via BTREE edge lookup — this is the function's main contribution over plain ANN. Configure via `WitnessCohortConfig(weights=WitnessCohortWeights(...), candidate_pool, min_witness_overlap, min_score, use_trajectory, bidirectional_check, timestamp_cutoff)`. Returns `WitnessCohortResult` with ranked `CohortMember` items, per-component scores, exclusion counts, and reproducibility metadata
|
|
135
|
+
| `find_witness_cohort(primary_key, pattern_id, top_n=10, *, config=None, edge_pattern_id=None)` | Rank entities that share the target's witness signature. Investigative peer ranking — NOT edge forecasting. Combines four signals: `exp(-distance/theta)` delta similarity, witness Jaccard overlap, trajectory cosine alignment (optional), and graded anomaly bonus from `delta_rank_pct`. Excludes entities already connected via BTREE edge lookup — this is the function's main contribution over plain ANN. Configure via `WitnessCohortConfig(weights=WitnessCohortWeights(...), candidate_pool, min_witness_overlap, min_score, use_trajectory, bidirectional_check, timestamp_cutoff)`. Returns `WitnessCohortResult` with ranked `CohortMember` items, per-component scores, exclusion counts, and reproducibility metadata |
|
|
136
136
|
|
|
137
137
|
### Analysis and Detection
|
|
138
138
|
|
|
@@ -149,7 +149,7 @@ with sphere.session("agent-1") as session:
|
|
|
149
149
|
| `cross_pattern_profile(primary_key, line_id)` | Anomaly status from all patterns the entity participates in |
|
|
150
150
|
| `find_chains_for_entity(primary_key, pattern_id, top_n)` | Find chains involving a specific entity |
|
|
151
151
|
| `find_neighborhood(primary_key, pattern_id, max_hops)` | BFS through polygon edges to find reachable entities |
|
|
152
|
-
| `find_counterparties(primary_key, line_id, from_col, to_col, pattern_id, top_n, use_edge_table=True)` | Discover counterparty entities from event data. When pattern_id is given and edge table exists, uses BTREE fast path with amount_sum/amount_max per counterparty |
|
|
152
|
+
| `find_counterparties(primary_key, line_id, from_col, to_col, pattern_id, top_n, use_edge_table=True, *, timestamp_cutoff=None)` | Discover counterparty entities from event data. When pattern_id is given and edge table exists, uses BTREE fast path with amount_sum/amount_max per counterparty. `timestamp_cutoff` applies to the edge-table fast path only; raises `GDSNavigationError` when supplied without an edge-table-eligible configuration |
|
|
153
153
|
| `assess_false_positive(primary_key, pattern_id)` | Evaluate likelihood of false positive anomaly classification |
|
|
154
154
|
|
|
155
155
|
**Detection recipes (population-level patterns):**
|
|
@@ -422,7 +422,7 @@ Or use the navigator shortcut: `nav.passive_scan(home_line_id)`.
|
|
|
422
422
|
| `add_points_source(name, line_id, rules, combine)` | Register a points-rule source filtering by column thresholds |
|
|
423
423
|
| `add_compound_source(name, geometry_pattern_id, line_id, rules)` | Geometry expansion intersected with points rules |
|
|
424
424
|
| `add_graph_source(name, pattern_id, contagion_threshold=0.3, weight)` | Register a graph contagion source — flags entities whose anomalous counterparty ratio exceeds threshold. Requires event pattern with edge table |
|
|
425
|
-
| `auto_discover(home_line_id, include_borderline)` | Auto-register all patterns related to a line. Also auto-detects graph sources for event patterns with edge tables |
|
|
425
|
+
| `auto_discover(home_line_id, include_borderline, *, include_graph=True)` | Auto-register all patterns related to a line. Also auto-detects graph sources for event patterns with edge tables. Pass `include_graph=False` to skip graph source registration when the downstream scan does not need contagion signal — `detect_cross_pattern_discrepancy` uses this to avoid ~37s-per-event-pattern edge-table reads |
|
|
426
426
|
| `scan(home_line_id, scoring, threshold, top_n)` | Execute batch scan across all registered sources. Returns `ScanResult` |
|
|
427
427
|
|
|
428
428
|
### ScanResult
|
|
@@ -224,6 +224,10 @@ The scanner collects, deduplicates, and ranks results into a unified candidate l
|
|
|
224
224
|
is the primary entry point for broad screening workflows where the agent does not yet know
|
|
225
225
|
which specific primitive to focus on. `auto_discover()` registers the appropriate sources
|
|
226
226
|
automatically — graph sources are added for every event pattern that has an edge table.
|
|
227
|
+
Callers that do not need graph contagion signal (e.g. `detect_cross_pattern_discrepancy`,
|
|
228
|
+
which measures geometry disagreement between patterns, not neighbor contamination) pass
|
|
229
|
+
`include_graph=False` to skip graph source registration and avoid the per-pattern edge
|
|
230
|
+
table read that would otherwise dominate scan latency on large spheres.
|
|
227
231
|
|
|
228
232
|
## Build Flow
|
|
229
233
|
|
|
@@ -278,7 +282,7 @@ The MCP package is a transport and tool layer.
|
|
|
278
282
|
It does not define the core GDS model. Instead, it:
|
|
279
283
|
|
|
280
284
|
- opens and closes spheres
|
|
281
|
-
- exposes
|
|
285
|
+
- exposes 67 navigation and analysis tools
|
|
282
286
|
- formats results as JSON
|
|
283
287
|
- manages session state for an agentic client
|
|
284
288
|
|
|
@@ -132,7 +132,7 @@ An edge table is a flat Lance dataset that links anchor entities through an even
|
|
|
132
132
|
- **Runtime graph traversal** -- `find_geometric_path` uses beam search over the edge table, scoring paths by geometric coherence of intermediate entities
|
|
133
133
|
- **Lazy chain discovery** -- `discover_chains` performs temporal BFS on edges without requiring build-time chain extraction
|
|
134
134
|
- **Edge statistics** -- row counts, unique entity counts, timestamp and amount ranges
|
|
135
|
-
- **Witness cohort discovery** -- `find_witness_cohort` ranks entities that share the target's witness signature by combining delta similarity, witness overlap, trajectory alignment, and a graded anomaly bonus, while excluding entities already connected via the edge table. **Investigative peer ranking, not edge forecasting
|
|
135
|
+
- **Witness cohort discovery** -- `find_witness_cohort` ranks entities that share the target's witness signature by combining delta similarity, witness overlap, trajectory alignment, and a graded anomaly bonus, while excluding entities already connected via the edge table. **Investigative peer ranking, not edge forecasting** — surfaces existing peers rather than predicting future edges. Uniquely possible because population-relative geometry, witness sets, temporal solids, and the edge table all live in one storage layer
|
|
136
136
|
|
|
137
137
|
**Edge table fields:** `from_key` and `to_key` are anchor entity keys derived from event pattern FK columns. `event_key` links back to the event line for traceability. `timestamp` is epoch seconds from the column specified by `edge_table.timestamp_col` (or auto-detected). `amount` is the numeric value from `edge_table.amount_col` (or auto-detected from columns named `amount`, `value`, `total`, `amt`). The semantic meaning of `amount` depends on the domain (e.g. payment value, fare, order total, shipment weight). When no amount column is found, defaults to 0.0.
|
|
138
138
|
|
|
@@ -2268,17 +2268,26 @@ class GDSNavigator:
|
|
|
2268
2268
|
line_id: str,
|
|
2269
2269
|
pattern_id: str,
|
|
2270
2270
|
top_n: int = 20,
|
|
2271
|
+
*,
|
|
2272
|
+
timestamp_cutoff: float | None = None,
|
|
2271
2273
|
) -> dict[str, Any]:
|
|
2272
2274
|
"""Fast counterparty lookup via edge table BTREE indexes.
|
|
2273
2275
|
|
|
2274
2276
|
Returns same structure as find_counterparties but with ``amount_sum``
|
|
2275
2277
|
and ``amount_max`` per counterparty entry. Anomaly enrichment uses
|
|
2276
2278
|
the resolved anchor pattern's geometry.
|
|
2279
|
+
|
|
2280
|
+
``timestamp_cutoff`` restricts the lookup to edges with
|
|
2281
|
+
``timestamp <= timestamp_cutoff``.
|
|
2277
2282
|
"""
|
|
2278
2283
|
# Outgoing: from_key == primary_key → counterparties in to_key
|
|
2279
|
-
fwd = self._storage.read_edges(
|
|
2284
|
+
fwd = self._storage.read_edges(
|
|
2285
|
+
pattern_id, from_keys=[primary_key], timestamp_to=timestamp_cutoff,
|
|
2286
|
+
)
|
|
2280
2287
|
# Incoming: to_key == primary_key → counterparties in from_key
|
|
2281
|
-
rev = self._storage.read_edges(
|
|
2288
|
+
rev = self._storage.read_edges(
|
|
2289
|
+
pattern_id, to_keys=[primary_key], timestamp_to=timestamp_cutoff,
|
|
2290
|
+
)
|
|
2282
2291
|
|
|
2283
2292
|
def _group(edges: pa.Table, group_col: str) -> list[dict[str, Any]]:
|
|
2284
2293
|
if edges.num_rows == 0:
|
|
@@ -2403,6 +2412,8 @@ class GDSNavigator:
|
|
|
2403
2412
|
pattern_id: str | None = None,
|
|
2404
2413
|
top_n: int = 20,
|
|
2405
2414
|
use_edge_table: bool = True,
|
|
2415
|
+
*,
|
|
2416
|
+
timestamp_cutoff: float | None = None,
|
|
2406
2417
|
) -> dict[str, Any]:
|
|
2407
2418
|
"""Find transaction counterparties of an entity with anomaly enrichment.
|
|
2408
2419
|
|
|
@@ -2413,6 +2424,13 @@ class GDSNavigator:
|
|
|
2413
2424
|
If *pattern_id* is provided, each counterparty is enriched with ``is_anomaly``
|
|
2414
2425
|
and ``delta_rank_pct`` from that pattern's geometry.
|
|
2415
2426
|
|
|
2427
|
+
``timestamp_cutoff`` restricts the lookup to edges with
|
|
2428
|
+
``timestamp <= timestamp_cutoff``. **Edge-table fast path only.** The
|
|
2429
|
+
points-scan fallback has no timestamp column available and cannot
|
|
2430
|
+
honor the cutoff, so passing it without an edge-table-eligible
|
|
2431
|
+
configuration raises ``GDSNavigationError`` to fail loudly instead of
|
|
2432
|
+
silently returning unfiltered results.
|
|
2433
|
+
|
|
2416
2434
|
Returns ``{primary_key, outgoing, incoming, summary}``.
|
|
2417
2435
|
"""
|
|
2418
2436
|
# Fast path: edge table BTREE lookup
|
|
@@ -2423,6 +2441,16 @@ class GDSNavigator:
|
|
|
2423
2441
|
):
|
|
2424
2442
|
return self._find_counterparties_via_edges(
|
|
2425
2443
|
primary_key, line_id, pattern_id, top_n,
|
|
2444
|
+
timestamp_cutoff=timestamp_cutoff,
|
|
2445
|
+
)
|
|
2446
|
+
|
|
2447
|
+
# Cutoff is meaningless on the points-scan fallback — fail loudly.
|
|
2448
|
+
if timestamp_cutoff is not None:
|
|
2449
|
+
raise GDSNavigationError(
|
|
2450
|
+
"find_counterparties: timestamp_cutoff is only supported on "
|
|
2451
|
+
"the edge-table fast path. Provide a pattern_id whose edge "
|
|
2452
|
+
"table exists and leave use_edge_table=True, or omit "
|
|
2453
|
+
"timestamp_cutoff."
|
|
2426
2454
|
)
|
|
2427
2455
|
|
|
2428
2456
|
sphere = self._storage.read_sphere()
|
|
@@ -2552,12 +2580,18 @@ class GDSNavigator:
|
|
|
2552
2580
|
primary_key: str,
|
|
2553
2581
|
pattern_id: str,
|
|
2554
2582
|
top_n: int = 20,
|
|
2583
|
+
*,
|
|
2584
|
+
timestamp_cutoff: float | None = None,
|
|
2555
2585
|
) -> dict[str, Any]:
|
|
2556
2586
|
"""Net flow analysis per counterparty via edge table.
|
|
2557
2587
|
|
|
2558
2588
|
Two edge lookups (outgoing + incoming), sum amounts, compute
|
|
2559
2589
|
per-counterparty net flow.
|
|
2560
2590
|
|
|
2591
|
+
When ``timestamp_cutoff`` is set (Unix seconds as float), only edges
|
|
2592
|
+
with ``timestamp <= timestamp_cutoff`` are considered — used for
|
|
2593
|
+
as-of evaluation of flow history up to a given point in time.
|
|
2594
|
+
|
|
2561
2595
|
Returns ``{outgoing_total, incoming_total, net_flow, flow_direction,
|
|
2562
2596
|
counterparties: [{key, net_flow, direction}]}``.
|
|
2563
2597
|
"""
|
|
@@ -2566,8 +2600,12 @@ class GDSNavigator:
|
|
|
2566
2600
|
f"Pattern '{pattern_id}' has no edge table. "
|
|
2567
2601
|
"entity_flow requires an edge table."
|
|
2568
2602
|
)
|
|
2569
|
-
fwd = self._storage.read_edges(
|
|
2570
|
-
|
|
2603
|
+
fwd = self._storage.read_edges(
|
|
2604
|
+
pattern_id, from_keys=[primary_key], timestamp_to=timestamp_cutoff,
|
|
2605
|
+
)
|
|
2606
|
+
rev = self._storage.read_edges(
|
|
2607
|
+
pattern_id, to_keys=[primary_key], timestamp_to=timestamp_cutoff,
|
|
2608
|
+
)
|
|
2571
2609
|
|
|
2572
2610
|
# Sum outgoing amounts per counterparty
|
|
2573
2611
|
out_by_cp: dict[str, float] = {}
|
|
@@ -2624,12 +2662,19 @@ class GDSNavigator:
|
|
|
2624
2662
|
self,
|
|
2625
2663
|
primary_key: str,
|
|
2626
2664
|
pattern_id: str,
|
|
2665
|
+
*,
|
|
2666
|
+
timestamp_cutoff: float | None = None,
|
|
2627
2667
|
) -> dict[str, Any]:
|
|
2628
2668
|
"""Score how many of an entity's counterparties are anomalous.
|
|
2629
2669
|
|
|
2630
2670
|
Edge lookup for counterparties, batch geometry check on the anchor
|
|
2631
2671
|
pattern. Score = anomalous_counterparties / total_counterparties.
|
|
2632
2672
|
|
|
2673
|
+
When ``timestamp_cutoff`` is set (Unix seconds as float), only edges
|
|
2674
|
+
with ``timestamp <= timestamp_cutoff`` are considered — used for
|
|
2675
|
+
as-of evaluation, reproducing the state of the graph at a given
|
|
2676
|
+
point in time.
|
|
2677
|
+
|
|
2633
2678
|
Returns ``{score: 0.0-1.0, total_counterparties,
|
|
2634
2679
|
anomalous_counterparties, interpretation}``.
|
|
2635
2680
|
"""
|
|
@@ -2638,8 +2683,12 @@ class GDSNavigator:
|
|
|
2638
2683
|
f"Pattern '{pattern_id}' has no edge table. "
|
|
2639
2684
|
"contagion_score requires an edge table."
|
|
2640
2685
|
)
|
|
2641
|
-
fwd = self._storage.read_edges(
|
|
2642
|
-
|
|
2686
|
+
fwd = self._storage.read_edges(
|
|
2687
|
+
pattern_id, from_keys=[primary_key], timestamp_to=timestamp_cutoff,
|
|
2688
|
+
)
|
|
2689
|
+
rev = self._storage.read_edges(
|
|
2690
|
+
pattern_id, to_keys=[primary_key], timestamp_to=timestamp_cutoff,
|
|
2691
|
+
)
|
|
2643
2692
|
|
|
2644
2693
|
cp_keys: set[str] = set()
|
|
2645
2694
|
if fwd.num_rows > 0:
|
|
@@ -2688,15 +2737,25 @@ class GDSNavigator:
|
|
|
2688
2737
|
primary_keys: list[str],
|
|
2689
2738
|
pattern_id: str,
|
|
2690
2739
|
max_keys: int = 200,
|
|
2740
|
+
*,
|
|
2741
|
+
timestamp_cutoff: float | None = None,
|
|
2691
2742
|
) -> dict[str, Any]:
|
|
2692
2743
|
"""Contagion score for multiple entities.
|
|
2693
2744
|
|
|
2745
|
+
When ``timestamp_cutoff`` is set, forwards it to each per-entity
|
|
2746
|
+
contagion_score call so that only edges with
|
|
2747
|
+
``timestamp < timestamp_cutoff`` are considered.
|
|
2748
|
+
|
|
2694
2749
|
Returns per-entity scores plus a summary with mean/max.
|
|
2695
2750
|
"""
|
|
2696
2751
|
keys = primary_keys[:max_keys]
|
|
2697
2752
|
results: list[dict[str, Any]] = []
|
|
2698
2753
|
for pk in keys:
|
|
2699
|
-
results.append(
|
|
2754
|
+
results.append(
|
|
2755
|
+
self.contagion_score(
|
|
2756
|
+
pk, pattern_id, timestamp_cutoff=timestamp_cutoff,
|
|
2757
|
+
)
|
|
2758
|
+
)
|
|
2700
2759
|
|
|
2701
2760
|
scores = [r["score"] for r in results]
|
|
2702
2761
|
mean_score = round(sum(scores) / len(scores), 4) if scores else 0.0
|
|
@@ -2718,12 +2777,19 @@ class GDSNavigator:
|
|
|
2718
2777
|
primary_key: str,
|
|
2719
2778
|
pattern_id: str,
|
|
2720
2779
|
n_buckets: int = 4,
|
|
2780
|
+
*,
|
|
2781
|
+
timestamp_cutoff: float | None = None,
|
|
2721
2782
|
) -> dict[str, Any]:
|
|
2722
2783
|
"""Temporal connection velocity via edge table.
|
|
2723
2784
|
|
|
2724
2785
|
Buckets edges by timestamp, counts unique counterparties per bucket.
|
|
2725
2786
|
Velocity = degree in last bucket / degree in first bucket.
|
|
2726
2787
|
|
|
2788
|
+
When ``timestamp_cutoff`` is set (Unix seconds as float), only edges
|
|
2789
|
+
with ``timestamp <= timestamp_cutoff`` are considered. Buckets are
|
|
2790
|
+
computed from the filtered edge set, so the last bucket endpoint is
|
|
2791
|
+
naturally <= cutoff.
|
|
2792
|
+
|
|
2727
2793
|
Returns ``{buckets: [{period, out_degree, in_degree}],
|
|
2728
2794
|
velocity_out, velocity_in, interpretation}``.
|
|
2729
2795
|
"""
|
|
@@ -2732,8 +2798,12 @@ class GDSNavigator:
|
|
|
2732
2798
|
f"Pattern '{pattern_id}' has no edge table. "
|
|
2733
2799
|
"degree_velocity requires an edge table."
|
|
2734
2800
|
)
|
|
2735
|
-
fwd = self._storage.read_edges(
|
|
2736
|
-
|
|
2801
|
+
fwd = self._storage.read_edges(
|
|
2802
|
+
pattern_id, from_keys=[primary_key], timestamp_to=timestamp_cutoff,
|
|
2803
|
+
)
|
|
2804
|
+
rev = self._storage.read_edges(
|
|
2805
|
+
pattern_id, to_keys=[primary_key], timestamp_to=timestamp_cutoff,
|
|
2806
|
+
)
|
|
2737
2807
|
|
|
2738
2808
|
# Collect all timestamps
|
|
2739
2809
|
fwd_ts = fwd["timestamp"].to_pylist() if fwd.num_rows > 0 else []
|
|
@@ -2883,6 +2953,8 @@ class GDSNavigator:
|
|
|
2883
2953
|
decay: float = 0.7,
|
|
2884
2954
|
min_threshold: float = 0.001,
|
|
2885
2955
|
max_affected: int = 10_000,
|
|
2956
|
+
*,
|
|
2957
|
+
timestamp_cutoff: float | None = None,
|
|
2886
2958
|
) -> dict[str, Any]:
|
|
2887
2959
|
"""BFS influence propagation from seed entities with geometric decay.
|
|
2888
2960
|
|
|
@@ -2890,6 +2962,12 @@ class GDSNavigator:
|
|
|
2890
2962
|
Stops expanding when score falls below *min_threshold* or when
|
|
2891
2963
|
*max_affected* entities have been reached.
|
|
2892
2964
|
|
|
2965
|
+
When ``timestamp_cutoff`` is set (Unix seconds as float), the BFS only
|
|
2966
|
+
follows edges with ``timestamp <= timestamp_cutoff``. Use this to
|
|
2967
|
+
reconstruct what influence propagation would have surfaced at a prior
|
|
2968
|
+
point in time — e.g. "what was reachable from this seed on the day
|
|
2969
|
+
of the incident?".
|
|
2970
|
+
|
|
2893
2971
|
Returns ``{seeds, affected_entities, summary}``.
|
|
2894
2972
|
"""
|
|
2895
2973
|
if not self._storage.has_edge_table(pattern_id):
|
|
@@ -2913,8 +2991,12 @@ class GDSNavigator:
|
|
|
2913
2991
|
def _expand_neighbors(keys: set[str]) -> None:
|
|
2914
2992
|
"""Read edges for keys and aggregate tx_count per neighbor."""
|
|
2915
2993
|
key_list = list(keys)
|
|
2916
|
-
fwd = self._storage.read_edges(
|
|
2917
|
-
|
|
2994
|
+
fwd = self._storage.read_edges(
|
|
2995
|
+
pattern_id, from_keys=key_list, timestamp_to=timestamp_cutoff,
|
|
2996
|
+
)
|
|
2997
|
+
rev = self._storage.read_edges(
|
|
2998
|
+
pattern_id, to_keys=key_list, timestamp_to=timestamp_cutoff,
|
|
2999
|
+
)
|
|
2918
3000
|
for tbl in (fwd, rev):
|
|
2919
3001
|
from_arr = tbl["from_key"].to_pylist()
|
|
2920
3002
|
to_arr = tbl["to_key"].to_pylist()
|
|
@@ -3304,9 +3386,9 @@ class GDSNavigator:
|
|
|
3304
3386
|
BTREE-indexed lookup. When ``bidirectional`` is True (default), inspects both
|
|
3305
3387
|
outgoing and incoming edges.
|
|
3306
3388
|
|
|
3307
|
-
``timestamp_max`` restricts the lookup to edges with
|
|
3308
|
-
|
|
3309
|
-
as-of state of the graph at a given point in time.
|
|
3389
|
+
``timestamp_max`` restricts the lookup to edges with
|
|
3390
|
+
``timestamp <= timestamp_max`` — used by hold-out evaluation to
|
|
3391
|
+
reproduce the as-of state of the graph at a given point in time.
|
|
3310
3392
|
"""
|
|
3311
3393
|
existing: set[str] = set()
|
|
3312
3394
|
fwd = self._storage.read_edges(
|
|
@@ -3389,16 +3471,10 @@ class GDSNavigator:
|
|
|
3389
3471
|
) -> WitnessCohortResult:
|
|
3390
3472
|
"""Rank entities that share ``primary_key``'s witness signature.
|
|
3391
3473
|
|
|
3392
|
-
**Investigative peer ranking, not edge forecasting.**
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
is_anomaly`` baseline (6.5%), 15.5% top-10 overlap with that
|
|
3397
|
-
baseline so results are substantively different. Temporal hold-out
|
|
3398
|
-
recall@10 is 0% — the function does NOT forecast which entities
|
|
3399
|
-
will form future edges; it surfaces entities that share the
|
|
3400
|
-
target's anomaly signature and are likely to belong to the same
|
|
3401
|
-
investigative cohort.
|
|
3474
|
+
**Investigative peer ranking, not edge forecasting.** Surfaces
|
|
3475
|
+
entities that share the target's anomaly signature and are likely
|
|
3476
|
+
to belong to the same investigative cohort. The function does NOT
|
|
3477
|
+
forecast which entities will form future edges.
|
|
3402
3478
|
|
|
3403
3479
|
Combines four signals into a composite score in [0, 1]:
|
|
3404
3480
|
|
|
@@ -3421,9 +3497,9 @@ class GDSNavigator:
|
|
|
3421
3497
|
so the cohort is denser in unknown peers worth investigating. When
|
|
3422
3498
|
``config.bidirectional_check`` is True (default) both outgoing and
|
|
3423
3499
|
incoming edges count as existing. ``config.timestamp_cutoff`` further
|
|
3424
|
-
restricts the existing-edge filter to edges with
|
|
3425
|
-
— used by hold-out evaluation to reproduce
|
|
3426
|
-
graph at a given point in time.
|
|
3500
|
+
restricts the existing-edge filter to edges with
|
|
3501
|
+
``timestamp <= cutoff`` — used by hold-out evaluation to reproduce
|
|
3502
|
+
the as-of state of the graph at a given point in time.
|
|
3427
3503
|
|
|
3428
3504
|
``edge_pattern_id`` overrides the auto-resolved event pattern; use this
|
|
3429
3505
|
when multiple event patterns share the same anchor and you want a
|
|
@@ -7626,7 +7702,12 @@ class GDSNavigator:
|
|
|
7626
7702
|
|
|
7627
7703
|
sphere = self._storage.read_sphere()
|
|
7628
7704
|
scanner = PassiveScanner(self._storage, sphere, self._manifest)
|
|
7629
|
-
|
|
7705
|
+
# Skip graph sources — this detector measures geometry disagreement
|
|
7706
|
+
# between patterns, not graph contagion. Registering graph sources
|
|
7707
|
+
# here triggers full edge-table reads per pattern (~37s on 5M-edge
|
|
7708
|
+
# spheres, compounding for multi-pattern lines) with zero signal
|
|
7709
|
+
# contribution to the downstream single-source hit check.
|
|
7710
|
+
scanner.auto_discover(entity_line, include_graph=False)
|
|
7630
7711
|
|
|
7631
7712
|
if len(scanner._sources) < 2:
|
|
7632
7713
|
return []
|
|
@@ -256,12 +256,17 @@ class PassiveScanner:
|
|
|
256
256
|
home_line_id: str,
|
|
257
257
|
include_borderline: bool = False,
|
|
258
258
|
borderline_rank_threshold: float = 80,
|
|
259
|
+
*,
|
|
260
|
+
include_graph: bool = True,
|
|
259
261
|
) -> PassiveScanner:
|
|
260
262
|
"""Auto-discover all patterns related to home_line_id.
|
|
261
263
|
|
|
262
264
|
Registers each discovered pattern as a source with auto-detected key_type.
|
|
263
265
|
When include_borderline is True, also registers borderline sources for
|
|
264
|
-
each direct pattern.
|
|
266
|
+
each direct pattern. When include_graph is False, skips registering
|
|
267
|
+
``{pattern_id}_graph`` sources even if event patterns with edge tables
|
|
268
|
+
exist — use this when the downstream scan does not need graph contagion
|
|
269
|
+
(e.g. cross-pattern geometry discrepancy detection).
|
|
265
270
|
"""
|
|
266
271
|
for pat_id in self._sphere.patterns:
|
|
267
272
|
key_type = self._classify_for_line(pat_id, home_line_id)
|
|
@@ -278,13 +283,14 @@ class PassiveScanner:
|
|
|
278
283
|
rank_threshold=borderline_rank_threshold,
|
|
279
284
|
)
|
|
280
285
|
# Auto-discover graph sources — event patterns with edge tables
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
if include_graph:
|
|
287
|
+
for pat_id, pat in self._sphere.patterns.items():
|
|
288
|
+
if pat.pattern_type == "event" and self._reader.has_edge_table(pat_id):
|
|
289
|
+
# Check if any relation targets home_line_id
|
|
290
|
+
for rel in pat.relations:
|
|
291
|
+
if rel.line_id == home_line_id:
|
|
292
|
+
self.add_graph_source(f"{pat_id}_graph", pat_id)
|
|
293
|
+
break
|
|
288
294
|
return self
|
|
289
295
|
|
|
290
296
|
def scan(
|
|
@@ -86,6 +86,7 @@ class _FakeHit:
|
|
|
86
86
|
primary_key: str
|
|
87
87
|
score: int
|
|
88
88
|
source_count: int = 1
|
|
89
|
+
weighted_score: float = 0.0
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
@dataclass
|
|
@@ -156,6 +157,30 @@ class TestDetectCrossPatternDiscrepancy:
|
|
|
156
157
|
assert result[0]["normal_patterns"] == ["pat_b"]
|
|
157
158
|
assert result[0]["delta_norm_anomalous"] == 3.5
|
|
158
159
|
|
|
160
|
+
def test_skips_graph_sources_in_auto_discover(self):
|
|
161
|
+
"""detect_cross_pattern_discrepancy must not register graph sources.
|
|
162
|
+
|
|
163
|
+
Graph contagion plays no role in detecting geometry disagreement
|
|
164
|
+
between patterns, but full edge-table scans are expensive on large
|
|
165
|
+
spheres (~37s per pattern on 5M-edge spheres). Confirm the detector
|
|
166
|
+
passes include_graph=False when calling auto_discover.
|
|
167
|
+
"""
|
|
168
|
+
nav = _make_navigator()
|
|
169
|
+
|
|
170
|
+
fake_scanner = MagicMock()
|
|
171
|
+
fake_scanner._sources = [MagicMock()] # only 1 source → early return
|
|
172
|
+
fake_scanner.auto_discover.return_value = fake_scanner
|
|
173
|
+
|
|
174
|
+
with patch(
|
|
175
|
+
"hypertopos.navigation.scanner.PassiveScanner",
|
|
176
|
+
return_value=fake_scanner,
|
|
177
|
+
):
|
|
178
|
+
nav.detect_cross_pattern_discrepancy("accounts")
|
|
179
|
+
|
|
180
|
+
fake_scanner.auto_discover.assert_called_once_with(
|
|
181
|
+
"accounts", include_graph=False,
|
|
182
|
+
)
|
|
183
|
+
|
|
159
184
|
|
|
160
185
|
class TestDetectNeighborContamination:
|
|
161
186
|
"""Tests for detect_neighbor_contamination."""
|