graphrefly 0.12.0__tar.gz → 0.13.0__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 (302) hide show
  1. {graphrefly-0.12.0 → graphrefly-0.13.0}/CHANGELOG.md +8 -0
  2. {graphrefly-0.12.0 → graphrefly-0.13.0}/PKG-INFO +1 -1
  3. {graphrefly-0.12.0 → graphrefly-0.13.0}/docs/optimizations.md +2 -0
  4. {graphrefly-0.12.0 → graphrefly-0.13.0}/docs/roadmap.md +2 -2
  5. {graphrefly-0.12.0 → graphrefly-0.13.0}/pyproject.toml +1 -1
  6. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/__init__.py +1 -0
  7. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/node.py +18 -6
  8. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/reduction.py +79 -29
  9. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_reduction.py +98 -0
  10. {graphrefly-0.12.0 → graphrefly-0.13.0}/.claude/skills/dev-dispatch/SKILL.md +0 -0
  11. {graphrefly-0.12.0 → graphrefly-0.13.0}/.claude/skills/parity/SKILL.md +0 -0
  12. {graphrefly-0.12.0 → graphrefly-0.13.0}/.claude/skills/qa/SKILL.md +0 -0
  13. {graphrefly-0.12.0 → graphrefly-0.13.0}/.gemini/skills/dev-dispatch/SKILL.md +0 -0
  14. {graphrefly-0.12.0 → graphrefly-0.13.0}/.gemini/skills/parity/SKILL.md +0 -0
  15. {graphrefly-0.12.0 → graphrefly-0.13.0}/.github/workflows/pages.yml +0 -0
  16. {graphrefly-0.12.0 → graphrefly-0.13.0}/.github/workflows/release.yml +0 -0
  17. {graphrefly-0.12.0 → graphrefly-0.13.0}/.gitignore +0 -0
  18. {graphrefly-0.12.0 → graphrefly-0.13.0}/.mise.toml +0 -0
  19. {graphrefly-0.12.0 → graphrefly-0.13.0}/CLAUDE.md +0 -0
  20. {graphrefly-0.12.0 → graphrefly-0.13.0}/CONTRIBUTING.md +0 -0
  21. {graphrefly-0.12.0 → graphrefly-0.13.0}/GEMINI.md +0 -0
  22. {graphrefly-0.12.0 → graphrefly-0.13.0}/LICENSE +0 -0
  23. {graphrefly-0.12.0 → graphrefly-0.13.0}/README.md +0 -0
  24. {graphrefly-0.12.0 → graphrefly-0.13.0}/archive/docs/DESIGN-ARCHIVE-INDEX.md +0 -0
  25. {graphrefly-0.12.0 → graphrefly-0.13.0}/archive/docs/SESSION-access-control-actor-guard.md +0 -0
  26. {graphrefly-0.12.0 → graphrefly-0.13.0}/archive/docs/SESSION-cross-repo-implementation-audit.md +0 -0
  27. {graphrefly-0.12.0 → graphrefly-0.13.0}/archive/docs/SESSION-demo-test-strategy.md +0 -0
  28. {graphrefly-0.12.0 → graphrefly-0.13.0}/archive/docs/SESSION-graphrefly-spec-design.md +0 -0
  29. {graphrefly-0.12.0 → graphrefly-0.13.0}/archive/docs/SESSION-serialization-memory-footprint.md +0 -0
  30. {graphrefly-0.12.0 → graphrefly-0.13.0}/archive/docs/SESSION-tier2-parity-nonlocal-forward-inner.md +0 -0
  31. {graphrefly-0.12.0 → graphrefly-0.13.0}/archive/docs/SESSION-universal-reduction-layer.md +0 -0
  32. {graphrefly-0.12.0 → graphrefly-0.13.0}/benchmarks/py-baseline.json +0 -0
  33. {graphrefly-0.12.0 → graphrefly-0.13.0}/docs/ADAPTER-CONTRACT.md +0 -0
  34. {graphrefly-0.12.0 → graphrefly-0.13.0}/docs/benchmark.md +0 -0
  35. {graphrefly-0.12.0 → graphrefly-0.13.0}/docs/docs-guidance.md +0 -0
  36. {graphrefly-0.12.0 → graphrefly-0.13.0}/docs/test-guidance.md +0 -0
  37. {graphrefly-0.12.0 → graphrefly-0.13.0}/examples/README.md +0 -0
  38. {graphrefly-0.12.0 → graphrefly-0.13.0}/examples/basic_counter.py +0 -0
  39. {graphrefly-0.12.0 → graphrefly-0.13.0}/llms.txt +0 -0
  40. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/__init__.py +0 -0
  41. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/compat/__init__.py +0 -0
  42. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/compat/async_utils.py +0 -0
  43. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/compat/asyncio_runner.py +0 -0
  44. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/compat/trio_runner.py +0 -0
  45. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/bridge.py +0 -0
  46. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/cancellation.py +0 -0
  47. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/clock.py +0 -0
  48. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/dynamic_node.py +0 -0
  49. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/guard.py +0 -0
  50. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/meta.py +0 -0
  51. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/protocol.py +0 -0
  52. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/runner.py +0 -0
  53. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/subgraph_locks.py +0 -0
  54. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/sugar.py +0 -0
  55. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/timer.py +0 -0
  56. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/core/versioning.py +0 -0
  57. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/__init__.py +0 -0
  58. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/adapters.py +0 -0
  59. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/backoff.py +0 -0
  60. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/backpressure.py +0 -0
  61. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/cascading_cache.py +0 -0
  62. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/checkpoint.py +0 -0
  63. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/composite.py +0 -0
  64. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/cron.py +0 -0
  65. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/data_structures.py +0 -0
  66. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/resilience.py +0 -0
  67. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/sources.py +0 -0
  68. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/tier1.py +0 -0
  69. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/extra/tier2.py +0 -0
  70. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/graph/__init__.py +0 -0
  71. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/graph/graph.py +0 -0
  72. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/integrations/__init__.py +0 -0
  73. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/integrations/django.py +0 -0
  74. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/integrations/fastapi.py +0 -0
  75. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/__init__.py +0 -0
  76. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/ai.py +0 -0
  77. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/cqrs.py +0 -0
  78. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/domain_templates.py +0 -0
  79. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/graphspec.py +0 -0
  80. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/memory.py +0 -0
  81. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/messaging.py +0 -0
  82. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/orchestration.py +0 -0
  83. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/reactive_layout/__init__.py +0 -0
  84. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/reactive_layout/measurement_adapters.py +0 -0
  85. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/reactive_layout/reactive_block_layout.py +0 -0
  86. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/patterns/reactive_layout/reactive_layout.py +0 -0
  87. {graphrefly-0.12.0 → graphrefly-0.13.0}/src/graphrefly/py.typed +0 -0
  88. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/bench_core.py +0 -0
  89. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/conftest.py +0 -0
  90. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_adapter_contract.py +0 -0
  91. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_adapters_ingest.py +0 -0
  92. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_adapters_storage.py +0 -0
  93. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_backpressure.py +0 -0
  94. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_bridge.py +0 -0
  95. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_cascading_cache.py +0 -0
  96. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_concurrency.py +0 -0
  97. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_core.py +0 -0
  98. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_django.py +0 -0
  99. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_domain_templates.py +0 -0
  100. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_dynamic_node.py +0 -0
  101. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_edge_cases.py +0 -0
  102. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_extra_composite.py +0 -0
  103. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_extra_data_structures.py +0 -0
  104. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_extra_resilience.py +0 -0
  105. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_extra_sources.py +0 -0
  106. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_extra_sources_http.py +0 -0
  107. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_extra_tier1.py +0 -0
  108. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_extra_tier2.py +0 -0
  109. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_fastapi.py +0 -0
  110. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_graph.py +0 -0
  111. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_graphspec.py +0 -0
  112. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_guard.py +0 -0
  113. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_measurement_adapters.py +0 -0
  114. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_patterns_ai.py +0 -0
  115. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_patterns_cqrs.py +0 -0
  116. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_patterns_memory.py +0 -0
  117. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_patterns_messaging.py +0 -0
  118. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_patterns_orchestration.py +0 -0
  119. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_patterns_reduction.py +0 -0
  120. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_perf_smoke.py +0 -0
  121. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_protocol.py +0 -0
  122. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_reactive_block_layout.py +0 -0
  123. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_reactive_layout.py +0 -0
  124. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_regressions.py +0 -0
  125. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_runner.py +0 -0
  126. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_smoke.py +0 -0
  127. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_sugar.py +0 -0
  128. {graphrefly-0.12.0 → graphrefly-0.13.0}/tests/test_versioning.py +0 -0
  129. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/.gitignore +0 -0
  130. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/README.md +0 -0
  131. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/astro.config.mjs +0 -0
  132. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/content.config.ts +0 -0
  133. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/package.json +0 -0
  134. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/pnpm-lock.yaml +0 -0
  135. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/public/llms.txt +0 -0
  136. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/py-api-sidebar.mjs +0 -0
  137. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/scripts/gen_api_docs.py +0 -0
  138. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/scripts/sync-docs.mjs +0 -0
  139. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/components/GraphreflyHero.astro +0 -0
  140. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/components/Header.astro +0 -0
  141. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/components/MobileMenuFooter.astro +0 -0
  142. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/components/PyodidePlayground.tsx +0 -0
  143. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/components/Sidebar.astro +0 -0
  144. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/components/SiteTitle.astro +0 -0
  145. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/BackoffPreset.md +0 -0
  146. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/BackoffStrategy.md +0 -0
  147. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/CheckpointAdapter.md +0 -0
  148. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/CircuitBreaker.md +0 -0
  149. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/CircuitOpenError.md +0 -0
  150. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/CompactEntry.md +0 -0
  151. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/DeferWhen.md +0 -0
  152. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/DictCheckpointAdapter.md +0 -0
  153. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/DistillBundle.md +0 -0
  154. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/EmitStrategy.md +0 -0
  155. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/Extraction.md +0 -0
  156. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/FileCheckpointAdapter.md +0 -0
  157. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/HttpBundle.md +0 -0
  158. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/JitterMode.md +0 -0
  159. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/MemoryCheckpointAdapter.md +0 -0
  160. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/Message.md +0 -0
  161. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/MessageType.md +0 -0
  162. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/Messages.md +0 -0
  163. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/NodeActions.md +0 -0
  164. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/NodeFn.md +0 -0
  165. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/NodeImpl.md +0 -0
  166. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/NodeStatus.md +0 -0
  167. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/PipeOperator.md +0 -0
  168. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/PubSubHub.md +0 -0
  169. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/ReactiveIndexBundle.md +0 -0
  170. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/ReactiveListBundle.md +0 -0
  171. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/ReactiveLogBundle.md +0 -0
  172. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/ReactiveMapBundle.md +0 -0
  173. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/SqliteCheckpointAdapter.md +0 -0
  174. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/SubscribeHints.md +0 -0
  175. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/TimeoutError.md +0 -0
  176. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/TokenBucket.md +0 -0
  177. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/VerifiableBundle.md +0 -0
  178. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/Versioned.md +0 -0
  179. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/WithBreakerBundle.md +0 -0
  180. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/WithStatusBundle.md +0 -0
  181. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/audit.md +0 -0
  182. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/batch.md +0 -0
  183. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/buffer.md +0 -0
  184. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/buffer_count.md +0 -0
  185. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/buffer_time.md +0 -0
  186. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/cache.md +0 -0
  187. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/cached.md +0 -0
  188. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/checkpoint_node_value.md +0 -0
  189. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/circuit_breaker.md +0 -0
  190. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/combine.md +0 -0
  191. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/concat.md +0 -0
  192. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/concat_map.md +0 -0
  193. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/constant.md +0 -0
  194. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/debounce.md +0 -0
  195. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/decorrelated_jitter.md +0 -0
  196. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/delay.md +0 -0
  197. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/derived.md +0 -0
  198. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/dispatch_messages.md +0 -0
  199. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/distill.md +0 -0
  200. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/distinct_until_changed.md +0 -0
  201. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/effect.md +0 -0
  202. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/element_at.md +0 -0
  203. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/emit_with_batch.md +0 -0
  204. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/empty.md +0 -0
  205. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/exhaust_map.md +0 -0
  206. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/exponential.md +0 -0
  207. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/fallback.md +0 -0
  208. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/fibonacci.md +0 -0
  209. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/filter.md +0 -0
  210. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/find.md +0 -0
  211. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/first.md +0 -0
  212. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/first_value_from.md +0 -0
  213. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/flat_map.md +0 -0
  214. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/for_each.md +0 -0
  215. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_any.md +0 -0
  216. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_async_iter.md +0 -0
  217. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_awaitable.md +0 -0
  218. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_cron.md +0 -0
  219. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_event_emitter.md +0 -0
  220. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_fs_watch.md +0 -0
  221. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_git_hook.md +0 -0
  222. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_http.md +0 -0
  223. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_iter.md +0 -0
  224. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_mcp.md +0 -0
  225. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_timer.md +0 -0
  226. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_webhook.md +0 -0
  227. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/from_websocket.md +0 -0
  228. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/gate.md +0 -0
  229. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/index.md +0 -0
  230. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/interval.md +0 -0
  231. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/is_batching.md +0 -0
  232. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/is_phase2_message.md +0 -0
  233. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/is_terminal_message.md +0 -0
  234. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/last.md +0 -0
  235. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/linear.md +0 -0
  236. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/log_slice.md +0 -0
  237. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/map.md +0 -0
  238. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/merge.md +0 -0
  239. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/message_tier.md +0 -0
  240. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/never.md +0 -0
  241. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/node.md +0 -0
  242. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/of.md +0 -0
  243. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/operator.md +0 -0
  244. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/pairwise.md +0 -0
  245. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/partition_for_batch.md +0 -0
  246. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/pausable.md +0 -0
  247. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/pipe.md +0 -0
  248. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/producer.md +0 -0
  249. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/propagates_to_meta.md +0 -0
  250. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/pubsub.md +0 -0
  251. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/race.md +0 -0
  252. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/rate_limiter.md +0 -0
  253. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/reactive_index.md +0 -0
  254. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/reactive_list.md +0 -0
  255. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/reactive_log.md +0 -0
  256. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/reactive_map.md +0 -0
  257. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/reduce.md +0 -0
  258. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/repeat.md +0 -0
  259. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/replay.md +0 -0
  260. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/rescue.md +0 -0
  261. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/resolve_backoff_preset.md +0 -0
  262. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/restore_graph_checkpoint.md +0 -0
  263. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/retry.md +0 -0
  264. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/sample.md +0 -0
  265. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/save_graph_checkpoint.md +0 -0
  266. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/scan.md +0 -0
  267. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/share.md +0 -0
  268. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/skip.md +0 -0
  269. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/start_with.md +0 -0
  270. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/state.md +0 -0
  271. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/subscribe.md +0 -0
  272. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/switch_map.md +0 -0
  273. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/take.md +0 -0
  274. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/take_until.md +0 -0
  275. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/take_while.md +0 -0
  276. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/tap.md +0 -0
  277. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/throttle.md +0 -0
  278. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/throw_error.md +0 -0
  279. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/timeout.md +0 -0
  280. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/timeout_node.md +0 -0
  281. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/to_array.md +0 -0
  282. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/to_list.md +0 -0
  283. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/to_sse.md +0 -0
  284. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/to_websocket.md +0 -0
  285. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/token_bucket.md +0 -0
  286. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/token_tracker.md +0 -0
  287. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/verifiable.md +0 -0
  288. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/window.md +0 -0
  289. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/window_count.md +0 -0
  290. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/window_time.md +0 -0
  291. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/with_breaker.md +0 -0
  292. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/with_latest_from.md +0 -0
  293. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/with_max_attempts.md +0 -0
  294. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/with_status.md +0 -0
  295. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/api/zip.md +0 -0
  296. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/index.mdx +0 -0
  297. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content/docs/lab/python.mdx +0 -0
  298. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/content.config.ts +0 -0
  299. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/env.d.ts +0 -0
  300. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/src/styles/custom.css +0 -0
  301. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/theme-prototypes.html +0 -0
  302. {graphrefly-0.12.0 → graphrefly-0.13.0}/website/tsconfig.json +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.13.0 (2026-04-07)
6
+
7
+ ### Features
8
+
9
+ - Fix stratify
10
+ ([`2dc57ff`](https://github.com/graphrefly/graphrefly-py/commit/2dc57ff6ddbbbfde6c4fc4e3bcdb12375f1eb640))
11
+
12
+
5
13
  ## v0.12.0 (2026-04-06)
6
14
 
7
15
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphrefly
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Reactive harness layer for agent workflows. Describe automations in plain language, trace every decision, enforce policies, persist checkpoints. Zero dependencies.
5
5
  Project-URL: Homepage, https://py.graphrefly.dev
6
6
  Project-URL: Repository, https://github.com/graphrefly/graphrefly-py
@@ -155,6 +155,8 @@ Union-find over node identity merges components when nodes list dependencies at
155
155
  - **Feedback effect perpetually dirty status (Phase 8.1/8.2, noted 2026-04-06 QA — documented, accepted):** The `__feedback_effect_<condition>` node forwards DIRTY via default dispatch but consumes DATA. The effect stays in "dirty" status between emissions. **By design** — the status is technically correct. Effects don't need to settle. These nodes now carry `meta._internal: True` and can be filtered from `describe()` output when undesired.
156
156
  - **Reduction primitives cross-language parity (Phase 8.1, noted 2026-04-06, QA 2026-04-06, updated 8.2 2026-04-06):** Both TS and PY implement `stratify`, `funnel`, `feedback`, `budget_gate`/`budgetGate`, and `scorer` in `patterns/reduction`. All five follow the orchestration factory pattern (`_base_meta`, `_register_step`). Key alignment: (1) `stratify` buffers DIRTY until DATA arrives — on classifier miss, emits `[DIRTY, RESOLVED]` to preserve spec §1.3.1 (both). (2) `funnel` now uses `bridge()` from `core/bridge` for inter-stage wiring — graph-visible effect nodes (`__bridge_<from>→<stage>_input`) replace the old subscribe-based bridges. Visible in `describe()`, participates in two-phase push. (3) `feedback` counter node is source of truth (resettable via `graph.set()`); feedback wiring now uses a graph-registered effect node (`__feedback_effect_<condition>`) instead of raw subscribe. Counter name is `__feedback_<condition>` to support multiple loops per graph. (4) `budget_gate`/`budgetGate` force-flushes all buffered items on terminal regardless of budget; sends RESUME before terminal if paused; forwards constraint ERROR downstream, silences constraint COMPLETE, forwards unknown constraint types via default. (5) `scorer` coerces `None`/`undefined` to 0 before multiplication (no TypeError/NaN divergence). TS `ScoredItem` is a plain object; PY `ScoredItem` is a class with `__slots__` and `__eq__`. Meta keys: `reduction: True`, `reduction_type: "<name>"`. Both repos: 22 tests each.
157
157
 
158
+ - **`stratify` two-dep gating (Phase 8.1/9.0, noted roadmap, resolved 2026-04-06, QA 2026-04-06):** `stratify` `on_message` previously intercepted only source messages (dep 0) and let rules messages (dep 1) fall through to default bitmask handling. When both source and rules updated in the same `batch()`, source DATA was classified against stale rules (rules DATA hadn't drained yet), and a spurious second emission occurred from default settlement of dep 1. Fix: `on_message` now intercepts **all** messages from both deps, implementing manual two-dep gating (same diamond-resolution pattern as `budget_gate`). Classification deferred until all dirty deps settle. Rules-only changes produce no downstream emission ("future items only"). QA hardening: (1) PAUSE/RESUME/INVALIDATE from rules dep swallowed (internal impl detail, not leaked downstream). (2) `classify` exceptions caught and treated as non-match (branch not killed). (3) `complete_when_deps_complete=False` explicit (branch lifecycle driven by source terminal, not auto-complete from both deps). Both TS and PY.
159
+
158
160
  - **Cleanup wrapper `{"cleanup": fn, "value"?: v}` (all phases, noted 2026-04-06, resolved 2026-04-06):** Node fn can now return `{"cleanup": callable, "value": v}` to explicitly separate cleanup from data. When returned: `cleanup` is registered; if `"value"` key is present, it's emitted as data. Plain callable returns remain cleanup for backward compat. Exported as `CleanupResult<T>` (TS) / documented dict pattern (PY). Resolves the documented limitation where returning a callable as data was silently consumed as cleanup.
159
161
 
160
162
  ---
@@ -26,7 +26,7 @@ These fix protocol violations identified post-8.2 ship. Must land before §9.2 a
26
26
 
27
27
  - [x] Rearchitect `feedback()` as graph-visible bridge node (8.2 → 9.0) — replaces subscribe-based shortcut; enables proper DIRTY→DATA two-phase on reentry/counter; resolves bare-DATA protocol gap
28
28
  - [x] Rearchitect `funnel()` bridges as graph-visible nodes (8.2 → 9.0) — replaces subscribe forwarding; resolves §5.9 imperative trigger violation + teardown leak
29
- - [ ] `stratify` two-dep gating (8.2 → 9.0) — gate classification on both source and rules settling (eliminates stale-rules race)
29
+ - [x] `stratify` two-dep gating (8.2 → 9.0) — gate classification on both source and rules settling (eliminates stale-rules race)
30
30
 
31
31
  ### Wave 2 (Python): Audit & accountability parity (Weeks 4-9)
32
32
 
@@ -555,7 +555,7 @@ Pre-wired graphs for common "info → action" domains. Users fork/extend.
555
555
  - [x] `data_quality_graph(opts)` → Graph — DB/API ingest → schema validation → anomaly detection → drift alerting → remediation suggestions
556
556
  - [x] Rearchitect `feedback()` as graph-visible bridge node (replaces subscribe-based shortcut; enables proper DIRTY→DATA two-phase on reentry/counter; resolves bare-DATA protocol gap)
557
557
  - [x] Rearchitect `funnel()` bridges as graph-visible nodes (replaces subscribe forwarding; resolves §5.9 imperative trigger violation + teardown leak)
558
- - [ ] `stratify` two-dep gating: gate classification on both source and rules settling (eliminates stale-rules race when both updated in same `batch()`)
558
+ - [x] `stratify` two-dep gating: gate classification on both source and rules settling (eliminates stale-rules race when both updated in same `batch()`)
559
559
 
560
560
  ### 8.3 — LLM graph composition
561
561
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "graphrefly"
3
- version = "0.12.0"
3
+ version = "0.13.0"
4
4
  description = "Reactive harness layer for agent workflows. Describe automations in plain language, trace every decision, enforce policies, persist checkpoints. Zero dependencies."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -26,6 +26,7 @@ from graphrefly.core.node import (
26
26
  NodeImpl,
27
27
  NodeStatus,
28
28
  SubscribeHints,
29
+ cleanup_result,
29
30
  node,
30
31
  )
31
32
  from graphrefly.core.protocol import (
@@ -106,13 +106,25 @@ def _status_after_message(status: NodeStatus, msg: Message) -> NodeStatus:
106
106
  type Message = tuple[Any, Any] | tuple[Any]
107
107
 
108
108
 
109
+ _CLEANUP_RESULT = "__graphrefly_cleanup_result__"
110
+ """Branded key that marks a :func:`cleanup_result` wrapper — prevents duck-type
111
+ collisions with domain dicts that happen to have a ``cleanup`` key."""
112
+
113
+
114
+ def cleanup_result(cleanup: Callable[[], None], value: Any = _SENTINEL) -> dict[str, Any]:
115
+ """Create a branded cleanup-result wrapper.
116
+
117
+ >>> node([dep], lambda vals, _: cleanup_result(release, computed))
118
+ """
119
+ r: dict[str, Any] = {_CLEANUP_RESULT: True, "cleanup": cleanup}
120
+ if value is not _SENTINEL:
121
+ r["value"] = value
122
+ return r
123
+
124
+
109
125
  def _is_cleanup_result(value: object) -> bool:
110
- """Check for explicit cleanup wrapper: ``{"cleanup": fn}`` or ``{"cleanup": fn, "value": v}``."""
111
- return (
112
- isinstance(value, dict)
113
- and "cleanup" in value
114
- and callable(value["cleanup"])
115
- )
126
+ """Check for branded cleanup wrapper created by :func:`cleanup_result`."""
127
+ return isinstance(value, dict) and value.get(_CLEANUP_RESULT) is True
116
128
 
117
129
 
118
130
  def _is_cleanup_fn(value: object) -> bool:
@@ -151,55 +151,105 @@ def _add_branch(
151
151
  ) -> None:
152
152
  """Add a stratify branch to the graph.
153
153
 
154
+ Two-dep gating: intercepts messages from **both** source (dep 0) and rules
155
+ (dep 1). Classification is deferred until all dirty deps have settled,
156
+ eliminating the stale-rules race when both are updated in the same
157
+ ``batch()``.
158
+
154
159
  Protocol: DIRTY is buffered until DATA arrives. If the classifier matches,
155
160
  emit [DIRTY, DATA]. If not, emit [DIRTY, RESOLVED] so downstream exits
156
161
  dirty status cleanly (spec §1.3.1). Source RESOLVED forwards as RESOLVED.
162
+ Rules-only changes produce no downstream emission ("future items only").
157
163
  """
158
164
  branch_name = f"branch/{rule.name}"
159
- pending_dirty = [False]
165
+ _no_value = object()
166
+
167
+ # Per-branch two-dep gating state
168
+ _source_dirty = [False]
169
+ _rules_dirty = [False]
170
+ _source_phase2 = [False] # source delivered DATA or RESOLVED this cycle
171
+ _source_value: list[Any] = [_no_value] # DATA payload, or _no_value for RESOLVED
172
+ _pending_dirty = [False] # owe downstream a DIRTY
173
+
174
+ def _resolve(actions: Any) -> None:
175
+ if _source_phase2[0]:
176
+ _source_phase2[0] = False
177
+ value = _source_value[0]
178
+ _source_value[0] = _no_value
179
+ if value is not _no_value:
180
+ # Source emitted DATA — classify with settled rules
181
+ current_rules: list[StratifyRule] = rules_node.get() or []
182
+ current_rule = next((r for r in current_rules if r.name == rule.name), None)
183
+ try:
184
+ matches = current_rule is not None and current_rule.classify(value)
185
+ except Exception:
186
+ matches = False
187
+ if matches:
188
+ _pending_dirty[0] = False
189
+ actions.emit(value)
190
+ else:
191
+ if _pending_dirty[0]:
192
+ _pending_dirty[0] = False
193
+ actions.down([(MessageType.DIRTY,), (MessageType.RESOLVED,)])
194
+ else:
195
+ # Source RESOLVED (unchanged)
196
+ if _pending_dirty[0]:
197
+ _pending_dirty[0] = False
198
+ actions.down([(MessageType.DIRTY,), (MessageType.RESOLVED,)])
199
+ else:
200
+ actions.down([(MessageType.RESOLVED,)])
201
+ # else: rules-only change — no reclassification ("future items only")
160
202
 
161
203
  def on_message(msg: Any, dep_index: int, actions: Any) -> bool:
162
- # Only intercept source messages (dep 0)
163
- if dep_index != 0:
164
- return False
165
-
166
204
  t = msg[0]
167
- if t is MessageType.DATA:
168
- value = msg[1]
169
- current_rules: list[StratifyRule] = rules_node.get() or []
170
- current_rule = next((r for r in current_rules if r.name == rule.name), None)
171
- if current_rule is not None and current_rule.classify(value):
172
- # Match: emit (actions.emit handles DIRTY+DATA wrapping)
173
- pending_dirty[0] = False
174
- actions.emit(value)
175
- else:
176
- # No match: emit DIRTY + RESOLVED so downstream exits dirty
177
- if pending_dirty[0]:
178
- pending_dirty[0] = False
179
- actions.down([(MessageType.DIRTY,), (MessageType.RESOLVED,)])
180
- return True
205
+
206
+ # --- DIRTY (phase 1) ---
181
207
  if t is MessageType.DIRTY:
182
- pending_dirty[0] = True
208
+ if dep_index == 0:
209
+ _source_dirty[0] = True
210
+ _pending_dirty[0] = True
211
+ else:
212
+ _rules_dirty[0] = True
183
213
  return True
184
- if t is MessageType.RESOLVED:
185
- # Source unchanged forward RESOLVED (with buffered DIRTY if any)
186
- if pending_dirty[0]:
187
- pending_dirty[0] = False
188
- actions.down([(MessageType.DIRTY,), (MessageType.RESOLVED,)])
214
+
215
+ # --- Phase 2 (DATA / RESOLVED) ---
216
+ if t is MessageType.DATA or t is MessageType.RESOLVED:
217
+ if dep_index == 0:
218
+ _source_dirty[0] = False
219
+ _source_phase2[0] = True
220
+ _source_value[0] = msg[1] if t is MessageType.DATA else _no_value
189
221
  else:
190
- actions.down([(MessageType.RESOLVED,)])
222
+ _rules_dirty[0] = False
223
+
224
+ # Wait for all dirty deps to settle
225
+ if _source_dirty[0] or _rules_dirty[0]:
226
+ return True
227
+
228
+ _resolve(actions)
191
229
  return True
230
+
231
+ # --- Terminal ---
192
232
  if t in (MessageType.COMPLETE, MessageType.ERROR, MessageType.TEARDOWN):
193
- pending_dirty[0] = False
194
- actions.down([msg])
233
+ _source_dirty[0] = False
234
+ _rules_dirty[0] = False
235
+ _source_phase2[0] = False
236
+ _source_value[0] = _no_value
237
+ _pending_dirty[0] = False
238
+ if dep_index == 0:
239
+ actions.down([msg])
240
+ # Rules terminal: swallow (branch stays alive)
195
241
  return True
196
- return False
242
+
243
+ # Swallow PAUSE/RESUME/INVALIDATE from rules dep (internal impl detail);
244
+ # forward unknown source messages via default handling.
245
+ return dep_index == 1
197
246
 
198
247
  filter_node = node(
199
248
  [source, rules_node],
200
249
  lambda _d, _a: None,
201
250
  on_message=on_message,
202
251
  describe_kind="operator",
252
+ complete_when_deps_complete=False,
203
253
  meta=_base_meta("stratify_branch", {"branch": rule.name}),
204
254
  )
205
255
 
@@ -112,6 +112,104 @@ class TestStratify:
112
112
  source.down([(MessageType.COMPLETE,)])
113
113
  assert completed[0] is True
114
114
 
115
+ def test_two_dep_gating_batch_source_and_rules(self) -> None:
116
+ """Stale-rules race: both source and rules update in same batch."""
117
+ source = state("x")
118
+ rules = [StratifyRule("match", classify=lambda v: v == "a")]
119
+
120
+ g = stratify("gate", source, rules)
121
+
122
+ seen: list[str] = []
123
+
124
+ def _sink(msgs: list) -> None:
125
+ for msg in msgs:
126
+ if msg[0] is MessageType.DATA:
127
+ seen.append(msg[1])
128
+
129
+ g.resolve("branch/match").subscribe(_sink)
130
+
131
+ # Batch: update rules to match "b" AND send source "b" simultaneously.
132
+ # Without two-dep gating, classification would use old rules (match "a")
133
+ # and "b" would be dropped. With gating, both settle first.
134
+ from graphrefly.core.protocol import batch
135
+
136
+ with batch():
137
+ g.set("rules", [StratifyRule("match", classify=lambda v: v == "b")])
138
+ source.down([(MessageType.DATA, "b")])
139
+
140
+ assert seen == ["b"]
141
+
142
+ def test_source_only_update_still_works(self) -> None:
143
+ """Regression: source-only updates must still classify correctly."""
144
+ source = state(0)
145
+ rules = [StratifyRule("pos", classify=lambda v: v > 0)]
146
+
147
+ g = stratify("src-only", source, rules)
148
+ seen: list[int] = []
149
+
150
+ def _sink(msgs: list) -> None:
151
+ for msg in msgs:
152
+ if msg[0] is MessageType.DATA:
153
+ seen.append(msg[1])
154
+
155
+ g.resolve("branch/pos").subscribe(_sink)
156
+
157
+ source.down([(MessageType.DATA, 5)])
158
+ source.down([(MessageType.DATA, -1)])
159
+ source.down([(MessageType.DATA, 3)])
160
+ assert seen == [5, 3]
161
+
162
+ def test_rules_only_update_no_downstream(self) -> None:
163
+ """Rules-only change should not trigger reclassification."""
164
+ source = state("a")
165
+ rules = [StratifyRule("match", classify=lambda v: v == "a")]
166
+
167
+ g = stratify("rules-only", source, rules)
168
+
169
+ emissions: list[object] = []
170
+
171
+ def _sink(msgs: list) -> None:
172
+ for msg in msgs:
173
+ emissions.append(msg)
174
+
175
+ g.resolve("branch/match").subscribe(_sink)
176
+
177
+ # Initial source DATA
178
+ source.down([(MessageType.DATA, "a")])
179
+ initial_count = len(emissions)
180
+
181
+ # Change rules only — should NOT produce any new downstream messages
182
+ g.set("rules", [StratifyRule("match", classify=lambda v: v == "b")])
183
+ assert len(emissions) == initial_count
184
+
185
+ def test_both_settle_source_resolved(self) -> None:
186
+ """Source DIRTY→RESOLVED + rules DATA in same batch → RESOLVED downstream."""
187
+ source = state("x")
188
+ rules = [StratifyRule("match", classify=lambda v: v == "x")]
189
+
190
+ g = stratify("resolved", source, rules)
191
+
192
+ data_seen: list[object] = []
193
+ resolved_count = [0]
194
+
195
+ def _sink(msgs: list) -> None:
196
+ for msg in msgs:
197
+ if msg[0] is MessageType.DATA:
198
+ data_seen.append(msg[1])
199
+ elif msg[0] is MessageType.RESOLVED:
200
+ resolved_count[0] += 1
201
+
202
+ g.resolve("branch/match").subscribe(_sink)
203
+
204
+ # Initial emission
205
+ source.down([(MessageType.DATA, "x")])
206
+ assert data_seen == ["x"]
207
+
208
+ # Now update rules in isolation — source not involved
209
+ g.set("rules", [StratifyRule("match", classify=lambda v: v == "y")])
210
+ # No new data should appear
211
+ assert data_seen == ["x"]
212
+
115
213
 
116
214
  # ---------------------------------------------------------------------------
117
215
  # funnel
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes