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.
Files changed (198) hide show
  1. {hypertopos-0.2.1 → hypertopos-0.2.2}/CHANGELOG.md +18 -7
  2. {hypertopos-0.2.1 → hypertopos-0.2.2}/PKG-INFO +1 -1
  3. {hypertopos-0.2.1 → hypertopos-0.2.2}/README.md +3 -3
  4. {hypertopos-0.2.1 → hypertopos-0.2.2}/ROADMAP.md +15 -3
  5. {hypertopos-0.2.1 → hypertopos-0.2.2}/SECURITY.md +1 -1
  6. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/api-reference.md +8 -8
  7. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/architecture.md +5 -1
  8. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/concepts.md +1 -1
  9. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/__init__.py +1 -1
  10. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/navigation/navigator.py +109 -28
  11. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/navigation/scanner.py +14 -8
  12. {hypertopos-0.2.1 → hypertopos-0.2.2}/pyproject.toml +1 -1
  13. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_detection.py +25 -0
  14. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_geometric_path.py +100 -0
  15. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_scanner.py +12 -0
  16. {hypertopos-0.2.1 → hypertopos-0.2.2}/.github/workflows/publish-pypi.yml +0 -0
  17. {hypertopos-0.2.1 → hypertopos-0.2.2}/.gitignore +0 -0
  18. {hypertopos-0.2.1 → hypertopos-0.2.2}/LICENSE.md +0 -0
  19. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/README.md +0 -0
  20. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/LICENSE +0 -0
  21. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/README.md +0 -0
  22. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/README.md +0 -0
  23. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/SUMMARY.md +0 -0
  24. {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
  25. {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
  26. {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
  27. {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
  28. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-settings.md +0 -0
  29. {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
  30. {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
  31. {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
  32. {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
  33. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-settings.md +0 -0
  34. {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
  35. {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
  36. {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
  37. {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
  38. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-settings.md +0 -0
  39. {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
  40. {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
  41. {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
  42. {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
  43. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-settings.md +0 -0
  44. {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
  45. {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
  46. {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
  47. {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
  48. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-settings.md +0 -0
  49. {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
  50. {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
  51. {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
  52. {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
  53. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-settings.md +0 -0
  54. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_accounts.py +0 -0
  55. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_cpty_banks.py +0 -0
  56. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_districts.py +0 -0
  57. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_loan_accounts.py +0 -0
  58. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_operations.py +0 -0
  59. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_orders.py +0 -0
  60. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_transactions.py +0 -0
  61. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/prepare_tx_types.py +0 -0
  62. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/berka/sphere/sphere.yaml +0 -0
  63. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/README.md +0 -0
  64. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/_layer2_cross_eval.py +0 -0
  65. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/_layer3_cross_eval.py +0 -0
  66. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_accounts.py +0 -0
  67. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_currencies.py +0 -0
  68. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_payment_formats.py +0 -0
  69. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_transactions.py +0 -0
  70. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/hi_small_sphere/sphere.yaml +0 -0
  71. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/layer1_passive_scan.py +0 -0
  72. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/layer2_xgboost_sweep.py +0 -0
  73. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/layer3_typology_filter.py +0 -0
  74. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_accounts.py +0 -0
  75. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_currencies.py +0 -0
  76. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_payment_formats.py +0 -0
  77. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_transactions.py +0 -0
  78. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/li_small_sphere/sphere.yaml +0 -0
  79. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/ibm-aml/passive-scan-calibration.md +0 -0
  80. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/README.md +0 -0
  81. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/SUMMARY.md +0 -0
  82. {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
  83. {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
  84. {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
  85. {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
  86. {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
  87. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-settings.md +0 -0
  88. {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
  89. {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
  90. {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
  91. {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
  92. {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
  93. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-settings.md +0 -0
  94. {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
  95. {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
  96. {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
  97. {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
  98. {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
  99. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-settings.md +0 -0
  100. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/sphere/prepare_trips.py +0 -0
  101. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmark/nyc-taxi/sphere/sphere.yaml +0 -0
  102. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/benchmarks.md +0 -0
  103. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/configuration.md +0 -0
  104. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/data-format.md +0 -0
  105. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/build-pipeline.svg +0 -0
  106. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/core-objects.svg +0 -0
  107. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/cutting-plane.svg +0 -0
  108. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/directory-layout.svg +0 -0
  109. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/hypertopos-overview.svg +0 -0
  110. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/model-lifecycle.svg +0 -0
  111. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/navigation-primitives.svg +0 -0
  112. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/images/three-scales.svg +0 -0
  113. {hypertopos-0.2.1 → hypertopos-0.2.2}/docs/quickstart.md +0 -0
  114. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/__init__.py +0 -0
  115. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/_stats.py +0 -0
  116. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/_writer.py +0 -0
  117. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/builder.py +0 -0
  118. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/derived.py +0 -0
  119. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/builder/mapping.py +0 -0
  120. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/__init__.py +0 -0
  121. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/__main__.py +0 -0
  122. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/build.py +0 -0
  123. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/info.py +0 -0
  124. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/main.py +0 -0
  125. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/schema.py +0 -0
  126. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/cli/sources.py +0 -0
  127. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/__init__.py +0 -0
  128. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/aggregation.py +0 -0
  129. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/calibration.py +0 -0
  130. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/chains.py +0 -0
  131. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/composition.py +0 -0
  132. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/datafusion_agg.py +0 -0
  133. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/forecast.py +0 -0
  134. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/geometry.py +0 -0
  135. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/invalidation.py +0 -0
  136. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/investigation.py +0 -0
  137. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/engine/subprocess_agg.py +0 -0
  138. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/model/__init__.py +0 -0
  139. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/model/manifest.py +0 -0
  140. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/model/objects.py +0 -0
  141. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/model/sphere.py +0 -0
  142. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/navigation/__init__.py +0 -0
  143. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/sphere.py +0 -0
  144. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/__init__.py +0 -0
  145. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/_schemas.py +0 -0
  146. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/cache.py +0 -0
  147. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/reader.py +0 -0
  148. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/storage/writer.py +0 -0
  149. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/utils/__init__.py +0 -0
  150. {hypertopos-0.2.1 → hypertopos-0.2.2}/hypertopos/utils/arrow.py +0 -0
  151. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/.gitkeep +0 -0
  152. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/conftest.py +0 -0
  153. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/fixtures/generate_fixtures.py +0 -0
  154. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_aggregation_engine.py +0 -0
  155. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_arrow_utils.py +0 -0
  156. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_builder.py +0 -0
  157. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_builder_alias.py +0 -0
  158. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_builder_derived.py +0 -0
  159. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_builder_edges.py +0 -0
  160. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_cache.py +0 -0
  161. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_calibration.py +0 -0
  162. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_chains.py +0 -0
  163. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_cli.py +0 -0
  164. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_cli_alias.py +0 -0
  165. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_cli_sources.py +0 -0
  166. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_compaction.py +0 -0
  167. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_composition.py +0 -0
  168. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_continuous_event_dims.py +0 -0
  169. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_data_quality_prop.py +0 -0
  170. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_datafusion_agg.py +0 -0
  171. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_edge_table.py +0 -0
  172. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_engine.py +0 -0
  173. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_forecast.py +0 -0
  174. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_fts.py +0 -0
  175. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_geometry_stats.py +0 -0
  176. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_hybrid_search.py +0 -0
  177. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_incremental.py +0 -0
  178. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_invalidation.py +0 -0
  179. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_investigation.py +0 -0
  180. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_mapping.py +0 -0
  181. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_model.py +0 -0
  182. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_mvcc_session.py +0 -0
  183. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_navigation.py +0 -0
  184. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_navigator_alerts.py +0 -0
  185. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_observability.py +0 -0
  186. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_precision_stack.py +0 -0
  187. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_reputation.py +0 -0
  188. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_row_id_cache.py +0 -0
  189. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_sphere.py +0 -0
  190. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_sphere_overview_perf.py +0 -0
  191. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_stats_guard.py +0 -0
  192. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_storage.py +0 -0
  193. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_streaming_reads.py +0 -0
  194. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_subprocess_agg.py +0 -0
  195. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_temporal_centroids.py +0 -0
  196. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_trajectory_ann.py +0 -0
  197. {hypertopos-0.2.1 → hypertopos-0.2.2}/tests/test_witness_cohort.py +0 -0
  198. {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: 3.7s → ~330ms on AML HI-small)
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.1
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
  [![PyArrow](https://img.shields.io/badge/format-PyArrow-red.svg)](https://arrow.apache.org/docs/python/)
9
9
  [![Lance](https://img.shields.io/badge/storage-Lance-blueviolet.svg)](https://github.com/lancedb/lancedb)
10
10
  [![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io)
11
- [![Version](https://img.shields.io/badge/version-0.2.1-blue.svg)](pyproject.toml)
11
+ [![Version](https://img.shields.io/badge/version-0.2.2-blue.svg)](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 validated on AML HI-small at **20.5× lift** over random base rate for co-laundering precision@10.
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 (validated 20.5× lift on AML) | Topological only (Adamic-Adar, common neighbors) | KNN, no graph awareness | Learned embeddings (training required) |
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.0
5
+ ## Current: 0.2.2
6
6
 
7
- **Graph meets geometry.** Lance-based **edge table** per event pattern with BTREE indexes, auto-emitted at build time, gives runtime graph traversal at O(log n) scale. 11 new navigator functions (paths, chains, flow, contagion, influence, bridges, anomalous edges, …) and matching MCP tools available in Phase 2. PassiveScanner gains a `"graph"` contagion source. 7 fraud-investigation recipes added to `gds-fraud-investigator`, all 8 skills refreshed for the new tools. Major perf wins (`passive_scan` 84 s → <10 s, `detect_pattern` 86 s → <15 s, `cluster_bridges` 31 s → <5 s). 66 MCP tools total (55 → 66).
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
- **First public release.** Complete GDS stack: declarative YAML sphere builder with CLI, navigation primitives π1–π12 (walk/jump/dive/attract/drift/trajectory/regime), aliases with cutting planes, temporal solids, investigation tools (`explain_anomaly`, witness/anti-witness, reputation), forecasting, FTS + hybrid search, online calibration tracker, MVCC sessions, multi-source PassiveScanner, incremental builder updates, MCP server (55 tools, smart detection, 3-phase visibility), Lance/Arrow storage, validation suite (Berka, NYC Taxi, IBM AML), full docs.
25
+ First public release — full GDS stack, π1–π12, builder, MCP server, validation suite. See CHANGELOG.
14
26
 
15
27
  ---
16
28
 
@@ -1,6 +1,6 @@
1
1
  # Security
2
2
 
3
- ## Current version: 0.2.x
3
+ ## Current version: 0.2.2
4
4
 
5
5
  hypertopos runs locally. No network services, no auth layer, no multi-tenancy.
6
6
 
@@ -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. Validated on AML HI-small: 20.5× lift over random for co-laundering precision@10 (25.3% vs 1.2%), 2.6× over `find_similar_entities + is_anomaly` baseline (6.5%), 15.5% top-10 overlap with that baseline |
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 66 navigation and analysis tools
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.** Validated on AML HI-small: 25.3% co-laundering precision@10 vs 1.2% random base rate (20.5× lift), 2.6× improvement over `find_similar_entities + is_anomaly` baseline (6.5%), 15.5% top-10 overlap with that baseline (substantively different results, not ANN with extra steps); temporal hold-out recall@10 is 0%, confirming the function 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
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
 
@@ -16,4 +16,4 @@ __all__ = [
16
16
  "GDSBuilder",
17
17
  "RelationSpec",
18
18
  ]
19
- __version__ = "0.1.0"
19
+ __version__ = "0.2.2"
@@ -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(pattern_id, from_keys=[primary_key])
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(pattern_id, to_keys=[primary_key])
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(pattern_id, from_keys=[primary_key])
2570
- rev = self._storage.read_edges(pattern_id, to_keys=[primary_key])
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(pattern_id, from_keys=[primary_key])
2642
- rev = self._storage.read_edges(pattern_id, to_keys=[primary_key])
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(self.contagion_score(pk, pattern_id))
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(pattern_id, from_keys=[primary_key])
2736
- rev = self._storage.read_edges(pattern_id, to_keys=[primary_key])
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(pattern_id, from_keys=key_list)
2917
- rev = self._storage.read_edges(pattern_id, to_keys=key_list)
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 timestamp strictly
3308
- less than the cutoff — used by hold-out evaluation to reproduce the
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.** Empirical
3393
- validation on AML HI-small (5M edges, 6357 labeled launderers):
3394
- 25.3% co-laundering precision@10 vs 1.2% random base rate
3395
- (20.5× lift), 2.6× improvement over ``find_similar_entities +
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 ``timestamp < cutoff``
3425
- — used by hold-out evaluation to reproduce the as-of state of the
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
- scanner.auto_discover(entity_line)
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
- for pat_id, pat in self._sphere.patterns.items():
282
- if pat.pattern_type == "event" and self._reader.has_edge_table(pat_id):
283
- # Check if any relation targets home_line_id
284
- for rel in pat.relations:
285
- if rel.line_id == home_line_id:
286
- self.add_graph_source(f"{pat_id}_graph", pat_id)
287
- break
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(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypertopos"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Navigate your data like space."
9
9
  requires-python = ">=3.12"
10
10
  license = {text = "BSL-1.1"}
@@ -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."""