graphrefly 0.5.0__tar.gz → 0.7.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 (287) hide show
  1. {graphrefly-0.5.0 → graphrefly-0.7.0}/CHANGELOG.md +24 -0
  2. {graphrefly-0.5.0 → graphrefly-0.7.0}/PKG-INFO +32 -16
  3. {graphrefly-0.5.0 → graphrefly-0.7.0}/README.md +30 -14
  4. {graphrefly-0.5.0 → graphrefly-0.7.0}/docs/optimizations.md +36 -8
  5. {graphrefly-0.5.0 → graphrefly-0.7.0}/docs/roadmap.md +3 -3
  6. {graphrefly-0.5.0/website/public → graphrefly-0.7.0}/llms.txt +3 -1
  7. {graphrefly-0.5.0 → graphrefly-0.7.0}/pyproject.toml +10 -8
  8. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/__init__.py +2 -0
  9. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/__init__.py +4 -0
  10. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/dynamic_node.py +27 -9
  11. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/guard.py +12 -2
  12. graphrefly-0.7.0/src/graphrefly/core/meta.py +246 -0
  13. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/node.py +44 -13
  14. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/protocol.py +3 -3
  15. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/sugar.py +4 -1
  16. graphrefly-0.7.0/src/graphrefly/core/timer.py +39 -0
  17. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/__init__.py +36 -0
  18. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/adapters.py +677 -118
  19. graphrefly-0.7.0/src/graphrefly/extra/cascading_cache.py +402 -0
  20. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/checkpoint.py +82 -63
  21. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/composite.py +22 -12
  22. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/resilience.py +275 -29
  23. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/graph/__init__.py +2 -0
  24. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/graph/graph.py +264 -61
  25. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/integrations/fastapi.py +26 -17
  26. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/ai.py +41 -25
  27. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/cqrs.py +31 -16
  28. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/orchestration.py +9 -6
  29. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_adapters_ingest.py +67 -79
  30. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_adapters_storage.py +498 -21
  31. graphrefly-0.7.0/tests/test_cascading_cache.py +261 -0
  32. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_core.py +4 -1
  33. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_extra_composite.py +19 -19
  34. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_extra_resilience.py +160 -8
  35. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_extra_sources.py +10 -6
  36. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_graph.py +257 -17
  37. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_guard.py +7 -4
  38. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_patterns_ai.py +12 -11
  39. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_patterns_cqrs.py +7 -5
  40. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_patterns_orchestration.py +2 -2
  41. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_reactive_layout.py +1 -1
  42. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_regressions.py +1 -1
  43. graphrefly-0.5.0/src/graphrefly/core/meta.py +0 -149
  44. {graphrefly-0.5.0 → graphrefly-0.7.0}/.claude/skills/dev-dispatch/SKILL.md +0 -0
  45. {graphrefly-0.5.0 → graphrefly-0.7.0}/.claude/skills/parity/SKILL.md +0 -0
  46. {graphrefly-0.5.0 → graphrefly-0.7.0}/.claude/skills/qa/SKILL.md +0 -0
  47. {graphrefly-0.5.0 → graphrefly-0.7.0}/.gemini/skills/dev-dispatch/SKILL.md +0 -0
  48. {graphrefly-0.5.0 → graphrefly-0.7.0}/.gemini/skills/parity/SKILL.md +0 -0
  49. {graphrefly-0.5.0 → graphrefly-0.7.0}/.github/workflows/pages.yml +0 -0
  50. {graphrefly-0.5.0 → graphrefly-0.7.0}/.github/workflows/release.yml +0 -0
  51. {graphrefly-0.5.0 → graphrefly-0.7.0}/.gitignore +0 -0
  52. {graphrefly-0.5.0 → graphrefly-0.7.0}/.mise.toml +0 -0
  53. {graphrefly-0.5.0 → graphrefly-0.7.0}/CLAUDE.md +0 -0
  54. {graphrefly-0.5.0 → graphrefly-0.7.0}/CONTRIBUTING.md +0 -0
  55. {graphrefly-0.5.0 → graphrefly-0.7.0}/GEMINI.md +0 -0
  56. {graphrefly-0.5.0 → graphrefly-0.7.0}/LICENSE +0 -0
  57. {graphrefly-0.5.0 → graphrefly-0.7.0}/archive/docs/DESIGN-ARCHIVE-INDEX.md +0 -0
  58. {graphrefly-0.5.0 → graphrefly-0.7.0}/archive/docs/SESSION-access-control-actor-guard.md +0 -0
  59. {graphrefly-0.5.0 → graphrefly-0.7.0}/archive/docs/SESSION-cross-repo-implementation-audit.md +0 -0
  60. {graphrefly-0.5.0 → graphrefly-0.7.0}/archive/docs/SESSION-demo-test-strategy.md +0 -0
  61. {graphrefly-0.5.0 → graphrefly-0.7.0}/archive/docs/SESSION-graphrefly-spec-design.md +0 -0
  62. {graphrefly-0.5.0 → graphrefly-0.7.0}/archive/docs/SESSION-serialization-memory-footprint.md +0 -0
  63. {graphrefly-0.5.0 → graphrefly-0.7.0}/archive/docs/SESSION-tier2-parity-nonlocal-forward-inner.md +0 -0
  64. {graphrefly-0.5.0 → graphrefly-0.7.0}/archive/docs/SESSION-universal-reduction-layer.md +0 -0
  65. {graphrefly-0.5.0 → graphrefly-0.7.0}/benchmarks/py-baseline.json +0 -0
  66. {graphrefly-0.5.0 → graphrefly-0.7.0}/docs/ADAPTER-CONTRACT.md +0 -0
  67. {graphrefly-0.5.0 → graphrefly-0.7.0}/docs/benchmark.md +0 -0
  68. {graphrefly-0.5.0 → graphrefly-0.7.0}/docs/docs-guidance.md +0 -0
  69. {graphrefly-0.5.0 → graphrefly-0.7.0}/docs/test-guidance.md +0 -0
  70. {graphrefly-0.5.0 → graphrefly-0.7.0}/examples/README.md +0 -0
  71. {graphrefly-0.5.0 → graphrefly-0.7.0}/examples/basic_counter.py +0 -0
  72. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/compat/__init__.py +0 -0
  73. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/compat/async_utils.py +0 -0
  74. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/compat/asyncio_runner.py +0 -0
  75. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/compat/trio_runner.py +0 -0
  76. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/cancellation.py +0 -0
  77. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/clock.py +0 -0
  78. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/runner.py +0 -0
  79. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/subgraph_locks.py +0 -0
  80. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/core/versioning.py +0 -0
  81. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/backoff.py +0 -0
  82. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/backpressure.py +0 -0
  83. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/cron.py +0 -0
  84. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/data_structures.py +0 -0
  85. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/sources.py +0 -0
  86. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/tier1.py +0 -0
  87. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/extra/tier2.py +0 -0
  88. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/integrations/__init__.py +0 -0
  89. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/__init__.py +0 -0
  90. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/memory.py +0 -0
  91. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/messaging.py +0 -0
  92. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/reactive_layout/__init__.py +0 -0
  93. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/reactive_layout/measurement_adapters.py +0 -0
  94. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/reactive_layout/reactive_block_layout.py +0 -0
  95. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/patterns/reactive_layout/reactive_layout.py +0 -0
  96. {graphrefly-0.5.0 → graphrefly-0.7.0}/src/graphrefly/py.typed +0 -0
  97. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/bench_core.py +0 -0
  98. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/conftest.py +0 -0
  99. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_adapter_contract.py +0 -0
  100. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_backpressure.py +0 -0
  101. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_concurrency.py +0 -0
  102. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_dynamic_node.py +0 -0
  103. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_edge_cases.py +0 -0
  104. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_extra_data_structures.py +0 -0
  105. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_extra_sources_http.py +0 -0
  106. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_extra_tier1.py +0 -0
  107. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_extra_tier2.py +0 -0
  108. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_fastapi.py +0 -0
  109. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_measurement_adapters.py +0 -0
  110. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_patterns_memory.py +0 -0
  111. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_patterns_messaging.py +0 -0
  112. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_perf_smoke.py +0 -0
  113. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_protocol.py +0 -0
  114. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_reactive_block_layout.py +0 -0
  115. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_runner.py +0 -0
  116. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_smoke.py +0 -0
  117. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_sugar.py +0 -0
  118. {graphrefly-0.5.0 → graphrefly-0.7.0}/tests/test_versioning.py +0 -0
  119. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/.gitignore +0 -0
  120. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/README.md +0 -0
  121. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/astro.config.mjs +0 -0
  122. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/content.config.ts +0 -0
  123. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/package.json +0 -0
  124. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/pnpm-lock.yaml +0 -0
  125. {graphrefly-0.5.0 → graphrefly-0.7.0/website/public}/llms.txt +0 -0
  126. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/py-api-sidebar.mjs +0 -0
  127. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/scripts/gen_api_docs.py +0 -0
  128. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/scripts/sync-docs.mjs +0 -0
  129. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/components/GraphreflyHero.astro +0 -0
  130. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/components/Header.astro +0 -0
  131. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/components/MobileMenuFooter.astro +0 -0
  132. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/components/PyodidePlayground.tsx +0 -0
  133. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/components/Sidebar.astro +0 -0
  134. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/components/SiteTitle.astro +0 -0
  135. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/BackoffPreset.md +0 -0
  136. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/BackoffStrategy.md +0 -0
  137. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/CheckpointAdapter.md +0 -0
  138. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/CircuitBreaker.md +0 -0
  139. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/CircuitOpenError.md +0 -0
  140. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/DeferWhen.md +0 -0
  141. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/DictCheckpointAdapter.md +0 -0
  142. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/DistillBundle.md +0 -0
  143. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/EmitStrategy.md +0 -0
  144. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/Extraction.md +0 -0
  145. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/FileCheckpointAdapter.md +0 -0
  146. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/HttpBundle.md +0 -0
  147. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/JitterMode.md +0 -0
  148. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/MemoryCheckpointAdapter.md +0 -0
  149. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/Message.md +0 -0
  150. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/MessageType.md +0 -0
  151. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/Messages.md +0 -0
  152. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/NodeActions.md +0 -0
  153. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/NodeFn.md +0 -0
  154. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/NodeImpl.md +0 -0
  155. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/NodeStatus.md +0 -0
  156. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/PipeOperator.md +0 -0
  157. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/PubSubHub.md +0 -0
  158. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/ReactiveIndexBundle.md +0 -0
  159. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/ReactiveListBundle.md +0 -0
  160. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/ReactiveLogBundle.md +0 -0
  161. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/ReactiveMapBundle.md +0 -0
  162. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/SqliteCheckpointAdapter.md +0 -0
  163. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/SubscribeHints.md +0 -0
  164. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/TokenBucket.md +0 -0
  165. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/VerifiableBundle.md +0 -0
  166. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/Versioned.md +0 -0
  167. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/WithBreakerBundle.md +0 -0
  168. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/WithStatusBundle.md +0 -0
  169. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/audit.md +0 -0
  170. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/batch.md +0 -0
  171. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/buffer.md +0 -0
  172. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/buffer_count.md +0 -0
  173. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/buffer_time.md +0 -0
  174. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/cached.md +0 -0
  175. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/checkpoint_node_value.md +0 -0
  176. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/circuit_breaker.md +0 -0
  177. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/combine.md +0 -0
  178. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/concat.md +0 -0
  179. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/concat_map.md +0 -0
  180. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/constant.md +0 -0
  181. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/debounce.md +0 -0
  182. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/decorrelated_jitter.md +0 -0
  183. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/delay.md +0 -0
  184. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/derived.md +0 -0
  185. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/dispatch_messages.md +0 -0
  186. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/distill.md +0 -0
  187. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/distinct_until_changed.md +0 -0
  188. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/effect.md +0 -0
  189. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/element_at.md +0 -0
  190. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/emit_with_batch.md +0 -0
  191. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/empty.md +0 -0
  192. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/exhaust_map.md +0 -0
  193. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/exponential.md +0 -0
  194. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/fibonacci.md +0 -0
  195. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/filter.md +0 -0
  196. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/find.md +0 -0
  197. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/first.md +0 -0
  198. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/first_value_from.md +0 -0
  199. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/flat_map.md +0 -0
  200. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/for_each.md +0 -0
  201. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_any.md +0 -0
  202. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_async_iter.md +0 -0
  203. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_awaitable.md +0 -0
  204. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_cron.md +0 -0
  205. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_event_emitter.md +0 -0
  206. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_fs_watch.md +0 -0
  207. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_git_hook.md +0 -0
  208. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_http.md +0 -0
  209. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_iter.md +0 -0
  210. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_mcp.md +0 -0
  211. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_timer.md +0 -0
  212. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_webhook.md +0 -0
  213. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/from_websocket.md +0 -0
  214. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/gate.md +0 -0
  215. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/index.md +0 -0
  216. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/interval.md +0 -0
  217. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/is_batching.md +0 -0
  218. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/is_phase2_message.md +0 -0
  219. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/is_terminal_message.md +0 -0
  220. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/last.md +0 -0
  221. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/linear.md +0 -0
  222. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/log_slice.md +0 -0
  223. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/map.md +0 -0
  224. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/merge.md +0 -0
  225. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/message_tier.md +0 -0
  226. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/never.md +0 -0
  227. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/node.md +0 -0
  228. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/of.md +0 -0
  229. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/operator.md +0 -0
  230. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/pairwise.md +0 -0
  231. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/partition_for_batch.md +0 -0
  232. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/pausable.md +0 -0
  233. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/pipe.md +0 -0
  234. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/producer.md +0 -0
  235. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/propagates_to_meta.md +0 -0
  236. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/pubsub.md +0 -0
  237. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/race.md +0 -0
  238. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/rate_limiter.md +0 -0
  239. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/reactive_index.md +0 -0
  240. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/reactive_list.md +0 -0
  241. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/reactive_log.md +0 -0
  242. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/reactive_map.md +0 -0
  243. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/reduce.md +0 -0
  244. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/repeat.md +0 -0
  245. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/replay.md +0 -0
  246. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/rescue.md +0 -0
  247. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/resolve_backoff_preset.md +0 -0
  248. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/restore_graph_checkpoint.md +0 -0
  249. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/retry.md +0 -0
  250. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/sample.md +0 -0
  251. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/save_graph_checkpoint.md +0 -0
  252. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/scan.md +0 -0
  253. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/share.md +0 -0
  254. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/skip.md +0 -0
  255. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/start_with.md +0 -0
  256. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/state.md +0 -0
  257. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/subscribe.md +0 -0
  258. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/switch_map.md +0 -0
  259. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/take.md +0 -0
  260. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/take_until.md +0 -0
  261. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/take_while.md +0 -0
  262. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/tap.md +0 -0
  263. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/throttle.md +0 -0
  264. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/throw_error.md +0 -0
  265. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/timeout.md +0 -0
  266. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/to_array.md +0 -0
  267. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/to_list.md +0 -0
  268. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/to_sse.md +0 -0
  269. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/to_websocket.md +0 -0
  270. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/token_bucket.md +0 -0
  271. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/token_tracker.md +0 -0
  272. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/verifiable.md +0 -0
  273. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/window.md +0 -0
  274. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/window_count.md +0 -0
  275. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/window_time.md +0 -0
  276. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/with_breaker.md +0 -0
  277. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/with_latest_from.md +0 -0
  278. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/with_max_attempts.md +0 -0
  279. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/with_status.md +0 -0
  280. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/api/zip.md +0 -0
  281. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/index.mdx +0 -0
  282. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content/docs/lab/python.mdx +0 -0
  283. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/content.config.ts +0 -0
  284. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/env.d.ts +0 -0
  285. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/src/styles/custom.css +0 -0
  286. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/theme-prototypes.html +0 -0
  287. {graphrefly-0.5.0 → graphrefly-0.7.0}/website/tsconfig.json +0 -0
@@ -2,6 +2,30 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.7.0 (2026-04-06)
6
+
7
+ ### Features
8
+
9
+ - Central timer util
10
+ ([`d1285d5`](https://github.com/graphrefly/graphrefly-py/commit/d1285d5c4c95928b1c27e10ca77204ee0f8aaa5e))
11
+
12
+ - Redesign initial + cached
13
+ ([`86d8506`](https://github.com/graphrefly/graphrefly-py/commit/86d850623e4e09fda1e8547ab67ca6e4cefb2a48))
14
+
15
+
16
+ ## v0.6.0 (2026-04-05)
17
+
18
+ ### Chores
19
+
20
+ - Inspector + data_class
21
+ ([`5bfb409`](https://github.com/graphrefly/graphrefly-py/commit/5bfb409eb62a82cc72d7b4d404981789ded3a3ca))
22
+
23
+ ### Features
24
+
25
+ - 3.1c
26
+ ([`914e424`](https://github.com/graphrefly/graphrefly-py/commit/914e424e7f934b71e407da187536c16cb63ee43a))
27
+
28
+
5
29
  ## v0.5.0 (2026-04-05)
6
30
 
7
31
  ### Chores
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphrefly
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Reactive graph protocol for human + LLM co-operation. Composable nodes, glitch-free diamond resolution, two-phase push, durable streaming. Zero dependencies.
5
5
  Project-URL: Homepage, https://py.graphrefly.dev
6
6
  Project-URL: Repository, https://github.com/graphrefly/graphrefly-py
@@ -8,7 +8,7 @@ Project-URL: Documentation, https://py.graphrefly.dev
8
8
  Author: David Chen
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
- Keywords: ai-agents,asyncio,callbag,diamond-resolution,durable-workflow,graph,llm,observable,orchestration,reactive,reactive-programming,signals,state-management,streaming,zero-dependency
11
+ Keywords: ai-agents,asyncio,automation,causal-tracing,durable-workflow,graph,human-in-the-loop,llm,natural-language,observable,orchestration,reactive,signals,state-management,streaming,workflow,zero-dependency
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Framework :: AsyncIO
14
14
  Classifier: Intended Audience :: Developers
@@ -25,9 +25,9 @@ Description-Content-Type: text/markdown
25
25
 
26
26
  # GraphReFly
27
27
 
28
- **Reactive graph protocol for human + LLM co-operation.**
28
+ **Describe what matters. It watches, filters, and explains — persistently.**
29
29
 
30
- One primitive. Zero dependencies. Composable nodes with glitch-free diamond resolution, two-phase push propagation, durable streaming, and async runners for asyncio and trio.
30
+ You're buried under emails, alerts, feeds, and messages. You can't process it all. GraphReFly lets you describe automations in plain language, review them visually, run them persistently, and trace every decision back to its source.
31
31
 
32
32
  [![PyPI](https://img.shields.io/pypi/v/graphrefly?color=blue)](https://pypi.org/project/graphrefly/)
33
33
  [![license](https://img.shields.io/github/license/graphrefly/graphrefly-py)](./LICENSE)
@@ -37,6 +37,18 @@ One primitive. Zero dependencies. Composable nodes with glitch-free diamond reso
37
37
 
38
38
  ---
39
39
 
40
+ <!-- TODO: Demo 0 GIF/video — NL → flow view → running → "why was this flagged?" -->
41
+
42
+ ## What can you do with it?
43
+
44
+ **Email triage** — "Watch my inbox. Urgent emails from my team go to a priority list. Newsletters get summarized weekly. Everything else, count by sender." It watches, classifies, and alerts — and when you ask "why was this flagged?", it walks you through the reasoning.
45
+
46
+ **Spending alerts** — Connect bank transactions to budget categories. Get a push notification when monthly dining exceeds your target. No polling, no manual checks — changes propagate the moment data arrives.
47
+
48
+ **Knowledge management** — Notes, bookmarks, highlights flow in. Contradictions surface automatically. Related ideas link themselves. Your second brain stays current without you maintaining it.
49
+
50
+ ---
51
+
40
52
  ## Quick start
41
53
 
42
54
  ```bash
@@ -56,20 +68,24 @@ count.push(3)
56
68
  # → doubled: 6
57
69
  ```
58
70
 
71
+ ## How it works
72
+
73
+ You describe what you need — an LLM composes a reactive graph (like SQL for data flows). The graph runs persistently, checkpoints its state, and traces every decision through a causal chain. Ask "why?" at any point and get a human-readable explanation from source to conclusion.
74
+
59
75
  ## Why GraphReFly?
60
76
 
61
- Most state libraries solve **one** problem well. GraphReFly solves the space between them:
62
-
63
- | | Redux / Zustand | RxPY | Pydantic AI | TC39 Signals | **GraphReFly** |
64
- |--|-----------------|------|-------------|-------------|---------------|
65
- | Simple store API | yes | no | no | yes | **yes** |
66
- | Streaming operators | no | yes | no | no | **yes** |
67
- | Diamond resolution | no | n/a | n/a | partial | **glitch-free** |
68
- | Graph introspection | no | no | no | no | **describe / observe / diagram** |
69
- | Durable checkpoints | no | no | no | no | **file / SQLite / IndexedDB** |
70
- | LLM orchestration | no | no | partial | no | **agent_loop / chat_stream / tool_registry** |
71
- | Async runners | n/a | asyncio | asyncio | n/a | **asyncio / trio** |
72
- | Dependencies | varies | 0 | many | n/a | **0** |
77
+ | | Redux / Zustand | RxPY | Pydantic AI | LangGraph | TC39 Signals | **GraphReFly** |
78
+ |--|-----------------|------|-------------|-----------|-------------|---------------|
79
+ | Simple store API | yes | no | no | no | yes | **yes** |
80
+ | Streaming operators | no | yes | no | no | no | **yes** |
81
+ | Diamond resolution | no | n/a | n/a | n/a | partial | **glitch-free** |
82
+ | Graph introspection | no | no | no | checkpoints | no | **describe / observe / diagram** |
83
+ | Causal tracing | no | no | no | no | no | **explain every decision** |
84
+ | Durable checkpoints | no | no | no | yes | no | **file / SQLite / IndexedDB** |
85
+ | LLM orchestration | no | no | partial | yes | no | **agent_loop / chat_stream / tool_registry** |
86
+ | NL graph composition | no | no | no | no | no | **graph_from_spec / llm_compose** |
87
+ | Async runners | n/a | asyncio | asyncio | asyncio | n/a | **asyncio / trio** |
88
+ | Dependencies | varies | 0 | many | many | n/a | **0** |
73
89
 
74
90
  ## One primitive
75
91
 
@@ -1,8 +1,8 @@
1
1
  # GraphReFly
2
2
 
3
- **Reactive graph protocol for human + LLM co-operation.**
3
+ **Describe what matters. It watches, filters, and explains — persistently.**
4
4
 
5
- One primitive. Zero dependencies. Composable nodes with glitch-free diamond resolution, two-phase push propagation, durable streaming, and async runners for asyncio and trio.
5
+ You're buried under emails, alerts, feeds, and messages. You can't process it all. GraphReFly lets you describe automations in plain language, review them visually, run them persistently, and trace every decision back to its source.
6
6
 
7
7
  [![PyPI](https://img.shields.io/pypi/v/graphrefly?color=blue)](https://pypi.org/project/graphrefly/)
8
8
  [![license](https://img.shields.io/github/license/graphrefly/graphrefly-py)](./LICENSE)
@@ -12,6 +12,18 @@ One primitive. Zero dependencies. Composable nodes with glitch-free diamond reso
12
12
 
13
13
  ---
14
14
 
15
+ <!-- TODO: Demo 0 GIF/video — NL → flow view → running → "why was this flagged?" -->
16
+
17
+ ## What can you do with it?
18
+
19
+ **Email triage** — "Watch my inbox. Urgent emails from my team go to a priority list. Newsletters get summarized weekly. Everything else, count by sender." It watches, classifies, and alerts — and when you ask "why was this flagged?", it walks you through the reasoning.
20
+
21
+ **Spending alerts** — Connect bank transactions to budget categories. Get a push notification when monthly dining exceeds your target. No polling, no manual checks — changes propagate the moment data arrives.
22
+
23
+ **Knowledge management** — Notes, bookmarks, highlights flow in. Contradictions surface automatically. Related ideas link themselves. Your second brain stays current without you maintaining it.
24
+
25
+ ---
26
+
15
27
  ## Quick start
16
28
 
17
29
  ```bash
@@ -31,20 +43,24 @@ count.push(3)
31
43
  # → doubled: 6
32
44
  ```
33
45
 
46
+ ## How it works
47
+
48
+ You describe what you need — an LLM composes a reactive graph (like SQL for data flows). The graph runs persistently, checkpoints its state, and traces every decision through a causal chain. Ask "why?" at any point and get a human-readable explanation from source to conclusion.
49
+
34
50
  ## Why GraphReFly?
35
51
 
36
- Most state libraries solve **one** problem well. GraphReFly solves the space between them:
37
-
38
- | | Redux / Zustand | RxPY | Pydantic AI | TC39 Signals | **GraphReFly** |
39
- |--|-----------------|------|-------------|-------------|---------------|
40
- | Simple store API | yes | no | no | yes | **yes** |
41
- | Streaming operators | no | yes | no | no | **yes** |
42
- | Diamond resolution | no | n/a | n/a | partial | **glitch-free** |
43
- | Graph introspection | no | no | no | no | **describe / observe / diagram** |
44
- | Durable checkpoints | no | no | no | no | **file / SQLite / IndexedDB** |
45
- | LLM orchestration | no | no | partial | no | **agent_loop / chat_stream / tool_registry** |
46
- | Async runners | n/a | asyncio | asyncio | n/a | **asyncio / trio** |
47
- | Dependencies | varies | 0 | many | n/a | **0** |
52
+ | | Redux / Zustand | RxPY | Pydantic AI | LangGraph | TC39 Signals | **GraphReFly** |
53
+ |--|-----------------|------|-------------|-----------|-------------|---------------|
54
+ | Simple store API | yes | no | no | no | yes | **yes** |
55
+ | Streaming operators | no | yes | no | no | no | **yes** |
56
+ | Diamond resolution | no | n/a | n/a | n/a | partial | **glitch-free** |
57
+ | Graph introspection | no | no | no | checkpoints | no | **describe / observe / diagram** |
58
+ | Causal tracing | no | no | no | no | no | **explain every decision** |
59
+ | Durable checkpoints | no | no | no | yes | no | **file / SQLite / IndexedDB** |
60
+ | LLM orchestration | no | no | partial | yes | no | **agent_loop / chat_stream / tool_registry** |
61
+ | NL graph composition | no | no | no | no | no | **graph_from_spec / llm_compose** |
62
+ | Async runners | n/a | asyncio | asyncio | asyncio | n/a | **asyncio / trio** |
63
+ | Dependencies | varies | 0 | many | many | n/a | **0** |
48
64
 
49
65
  ## One primitive
50
66
 
@@ -124,13 +124,41 @@ Union-find over node identity merges components when nodes list dependencies at
124
124
  - **~~Sink adapter silent error swallowing (Phase 5.2b–5.2d, noted 2026-04-04, resolved 2026-04-04):~~** All per-record sinks now return a `SinkHandle` (or `BufferedSinkHandle`) with a reactive `errors` companion node. The `_create_sink_error_handler()` factory always writes to the errors node; if a user callback is also provided, it fires first. `dispose()` sends `TEARDOWN` to the errors node. Mirrors TS implementation. Re-entrant batch drain protection applied via `contextlib.suppress(Exception)` around `errors_node.down()`.
125
125
  - **~~TS buffered sinks missing ERROR flush (Phase 5.3c, noted 2026-04-04, resolved 2026-04-04):~~** Both TS and PY buffered sinks now use `messageTier(msg[0]) >= 3` / `message_tier(msg[0]) >= 3` to flush on any terminal (COMPLETE, ERROR, TEARDOWN). Previously both used hardcoded `COMPLETE || TEARDOWN` checks and silently dropped buffered data on upstream ERROR.
126
126
  - **~~Synchronous SQLite blocking in `to_sqlite` sink (Phase 5.2b, noted 2026-04-04, resolved 2026-04-04):~~** Both TS and PY now accept `batch_insert` / `batchInsert` (default `False`) and `max_batch_size` / `maxBatchSize` (default `1000`). DATA values are buffered and flushed inside a `BEGIN`/`COMMIT` transaction on terminal messages (`message_tier >= 3`), at `max_batch_size` threshold, or on `dispose()`. On insert error, the first error triggers `break` + `ROLLBACK`. BEGIN failure preserves pending data for retry via manual `flush()`. Both return `BufferedSinkHandle` when batch mode is enabled.
127
+ - **CheckpointAdapter refactored to key-value (Phase 3.1c, noted 2026-04-05, resolved 2026-04-05):** `CheckpointAdapter` changed from blob-based (`save(data)` / `load()`) to key-value (`save(key, data)` / `load(key)` / `clear(key)`). Unifies checkpoint persistence and cache storage under one interface. Both TS and PY updated. `save_graph_checkpoint` / `restore_graph_checkpoint` pass `graph.name` as key. `auto_checkpoint` passes `self.name` as key. `FileCheckpointAdapter` now takes a directory (one file per key, sanitized filenames). `DictCheckpointAdapter` no longer takes an internal key in constructor. `SqliteCheckpointAdapter` no longer takes a fixed key — caller provides it. Pre-1.0, all downstream consumers updated, no legacy shims.
128
+ - **`tiered_storage` uses `CheckpointAdapter[]` directly (Phase 3.1c, noted 2026-04-05, resolved 2026-04-05):** With key-value `CheckpointAdapter`, `tiered_storage` wraps adapters as `CacheTier`s naturally. No separate `CacheTier` interface needed for end users — `CheckpointAdapter` serves both checkpoint and cache use cases.
129
+ - **~~`equals` must never see undefined/None (all phases, noted 2026-04-05, resolved 2026-04-05):~~** Superseded by `_SENTINEL` / `NO_VALUE` sentinel below. Original fix used `_has_emitted_data` flag; the sentinel approach replaced both.
130
+ - **~~`_cached is None` sentinel is ambiguous with `[(DATA, None)]` and INVALIDATE (noted 2026-04-05, resolved 2026-04-05):~~** Superseded by `_SENTINEL` / `NO_VALUE` sentinel below.
131
+ - **~~Derived node error observability gap (all phases, noted 2026-04-05, resolved 2026-04-05):~~** `fn`/`equals`/`on_message` errors now wrapped with `RuntimeError(f'Node "{name}": fn threw')` / `equals threw` / `on_message threw` with `__cause__`. Both TS and PY.
132
+ - **~~PY `retry` lock scope around `connect()` (Phase 3.1, noted 2026-04-05, resolved 2026-04-05):~~** Verified that `connect()` already runs outside the lock. Added clarifying comment.
133
+ - **PY `SqliteCheckpointAdapter` default `check_same_thread=True` (Phase 3.1, noted 2026-04-05):** Python's `sqlite3.connect()` defaults to `check_same_thread=True`, raising `ProgrammingError` if `save()` is called from a timer/debounce thread while `load()` runs on the main thread. `check_same_thread=False` + `threading.Lock` fix was prepared but reverted by linter. Deferred: re-apply when multi-threaded checkpoint usage becomes a pattern.
134
+ - **~~Payload-less DATA spec note (Phase 5.2–5.3, noted 2026-04-05, resolved 2026-04-05):~~** Spec clarified: payload-less DATA (`(DATA,)`) is not valid per protocol — all DATA tuples must carry a payload. Sink `msg[1] if len(msg) > 1 else None` coercion retained as defensive guard; upstream producers are the source of truth.
135
+ - **~~Sink `dispose()` error routing (Phase 5.2–5.3, noted 2026-04-05, resolved 2026-04-05):~~** All 20 PY sink `dispose()` functions now route errors to the `errors` companion node before suppressing.
136
+ - **~~`from_django_orm` and `from_tortoise` have identical implementations (Phase 5.2, noted 2026-04-05, resolved 2026-04-05):~~** Extracted `_from_sync_rows` helper. Both entry points kept with distinct validation.
137
+ - **~~Auto-edge registration from constructor deps (all phases, noted 2026-04-05, resolved 2026-04-05):~~** `Graph.add()` now auto-registers edges from constructor deps. Eliminates dual-bookkeeping bug surface.
138
+ - **~~ResettableTimer primitive (Phase 3.1, noted 2026-04-05, resolved 2026-04-05):~~** `ResettableTimer` in `core/timer.py`. `retry`, `rate_limiter`, `timeout` refactored to use it.
139
+ - **~~`to_json()` → `to_dict()` + `to_json_string()` (Phase 1.4, noted 2026-04-05, resolved 2026-04-05):~~** PY: `to_json()` renamed to `to_json_string()`; `to_dict()` added as alias of `snapshot()`; `to_json` alias removed (pre-1.0, no backward compat needed). TS: `toJSON()` renamed to `toObject()`; `toJSON()` kept as ECMAScript hook. Spec §3.8 updated.
140
+ - **~~Initial value, cached state, and equals interaction (all phases, noted 2026-04-05, resolved 2026-04-05):~~** Resolved with `_SENTINEL` / `NO_VALUE` sentinel (TS: `Symbol.for("graphrefly/NO_VALUE")`, PY: `_SENTINEL = object()`). Replaces the `_has_emitted_data` boolean flag entirely. One field (`_cached`) instead of two — impossible to desync. Key semantics: (1) When `initial` option is present (even as `None`), `_cached = initial` — `equals` IS called on first emission. (2) When `initial` is absent, `_cached = _SENTINEL` — first emission always DATA. (3) INVALIDATE / `reset_on_teardown` set `_cached = _SENTINEL`. (4) Resubscribable: terminal reset now also sets `_cached = _SENTINEL` — new subscriber always gets DATA. (5) Reconnect: cache retained → same-value emits RESOLVED — correct. (6) `get()` returns `None` when `_cached is _SENTINEL`. Spec §2.5 updated.
141
+ - **Auto-edge registration is local-only (Phase 1.1, noted 2026-04-05):** `Graph.add()` auto-registers edges for deps within the same `Graph` instance only. Cross-subgraph deps still require explicit `connect()`. Consistent with spec: cross-subgraph edges are explicit wiring.
127
142
 
128
143
  ---
129
144
 
145
+ ## Cross-language parity fixes (2026-04-05)
146
+
147
+ - **~~PY error wrapping missing original message (resolved 2026-04-05):~~** `RuntimeError(f'Node "{name}": fn threw')` omitted the original error message. TS included it: `fn threw: ${errMsg}`. Fixed all 3 PY sites (`fn threw`, `equals threw`, `on_message threw`) to include `{err}`. TS `onMessage` error was also not wrapped — now wraps as `Node "${name}": onMessage threw: ${errMsg}` with `{ cause }`.
148
+ - **~~PY `Graph.add()` missing reverse edge scan (resolved 2026-04-05):~~** PY only did forward auto-edge registration. TS did both forward + reverse (existing nodes whose deps include the new node). Fixed: PY now does the reverse scan, matching TS.
149
+ - **~~PY `_emit_sequential` terminal routing (resolved 2026-04-05):~~** PY appended terminals to the same queue as DATA/RESOLVED. TS routed them to `pendingPhase3`. Fixed: PY terminals now go to `bs.pending_phase3`.
150
+ - **~~PY `emit_with_batch` default strategy (resolved 2026-04-05):~~** PY defaulted to `"sequential"`, TS to `"partition"`. Fixed: PY default changed to `"partition"`.
151
+ - **~~PY `retry` sink missing `stopped` guard (resolved 2026-04-05):~~** TS checked `if (stopped) return;` at top of retry subscribe callback. PY had no such guard. Fixed: added `if stopped[0]: return`.
152
+ - **~~PY `fallback` plain-value path used `down()` (resolved 2026-04-05):~~** TS used `a.emit(fb)` (goes through equals + DIRTY/DATA wrapping). PY used `actions.down([(DATA, fb), (COMPLETE,)])` (bypassed equals). Fixed: PY now uses `actions.emit(fb)` then `actions.down([(COMPLETE,)])`.
153
+
130
154
  ## Cross-language implementation notes
131
155
 
132
156
  **Keep this section in sync with `graphrefly-ts/docs/optimizations.md` § Cross-language implementation notes** so you can open both files side by side.
133
157
 
158
+ - **~~Dual-bookkeeping of derived deps + connect edges (all phases, noted 2026-04-05, resolved 2026-04-05):~~** `Graph.add()` now auto-registers edges from constructor deps in both TS and PY. Eliminates the divergence risk between node-level deps and graph-level edge registry. All graph factories no longer need explicit `connect()` calls for constructor deps.
159
+
160
+ - **Resilience composition parity (Phase 3.1c, 2026-04-05):** Both TS and PY implement `fallback(source, fb)`, `timeout(source, timeoutNs/timeout_ns)`, `cache(source, ttlNs/ttl_ns)`, `cascadingCache`/`cascading_cache`, and `tieredStorage`/`tiered_storage`. `CacheEvictionPolicy`/`EvictionPolicy` use aligned method names: `insert`/`touch`/`delete`/`evict(count)`/`size()`. `CacheTier`: `load` is required; `save`/`clear` are optional (TS `?` modifier; PY uses `hasattr` checks). `tieredStorage`/`tiered_storage` returns a wrapper with `.cache` property exposing the inner `CascadingCache` in both languages. Eviction demotes to deepest tier with `save` before removing — value is preserved in cold storage. Cache miss sentinel: TS `undefined`, PY `None`. `TimeoutError`: TS extends `Error`; PY subclasses `builtins.TimeoutError` so `except TimeoutError` catches both. Validation: TS `RangeError`, PY `ValueError` — language convention. `cache()` replay emits raw `DATA` (no DIRTY/RESOLVED) on both sides. §5.10 timer exception: `timeout`, `retry`, `rateLimiter`/`rate_limiter` now use `ResettableTimer` (`core/timer.py` in PY, `core/timer.ts` in TS) — a reusable primitive extracted from the repeated `setTimeout`/`threading.Timer` pattern.
161
+ - **Progressive disclosure parity (Phase 3.3b, 2026-04-04):** Both TS and PY `describe()` support `detail` (`"minimal"` / `"standard"` / `"full"`) and `fields` (GraphQL-style field selection with dotted meta paths like `"meta.label"`). Default changed from returning all fields to `"minimal"` (type + deps only) in both languages — pre-1.0, no backward compat concern. `format: "spec"` / `format="spec"` forces minimal fields. Both return an `expand()` method on the result for re-reading with higher detail. TS `expand` is a property on the result object; PY `describe()` returns a `DescribeResult` (dict subclass) with `expand()` as a method — `json.dumps(graph.describe())` works safely in both languages (TS `JSON.stringify` drops functions; PY `expand` is not a dict key). `observe()` supports `detail` with the same three levels: `"minimal"` (DATA events only in events list, counts still tracked), `"standard"` (current behavior), `"full"` (implies structured + timeline + causal + derived). `ObserveResult` has `expand()` in both languages. TS `expand` is a method on the result object; PY `expand` is a method on the `ObserveResult` dataclass. Internal callers (`snapshot`, `auto_checkpoint`, `dump_graph`, AI pattern functions) explicitly pass the detail level they need. Diagram methods (`to_mermaid`/`toMermaid`, `to_d2`/`toD2`) only use paths/edges so minimal default is fine — intentional, no detail option needed. `"standard"` includes versioning (`v`); `"full"` adds `guard` and `last_mutation`/`lastMutation` (runtime attribution, not restored by `restore()`). `snapshot()` / `auto_checkpoint` strip `last_mutation`/`guard` from persisted nodes so snapshot → restore → snapshot is idempotent — use `describe(detail="full")` for audit snapshots that include attribution. Unrecognized `detail` strings silently fall back to `"minimal"` — no runtime validation; we'll revisit if real-world usage shows this causes confusion. Dict/object filters (`meta_has`, `status`, etc.) operate on whatever fields the chosen detail level provides; at `"minimal"`, fields like `meta` and `status` are absent, so filters that depend on them silently exclude all nodes. Users should pass `detail="standard"` or higher when using these filters. Spec Appendix B `status` field changed from `required` to optional (schema applies at `detail >= "standard"`).
134
162
  - **SQLite adapter parity (Phase 5.2b, 2026-04-04):** Both TS and PY use duck-typed `SqliteDbLike` with a `query(sql, params)` method — matching the `PostgresClientLike`/`ClickHouseClientLike` convention. TS `SqliteDbLike.query()` returns `unknown[]`; PY `SqliteDbLike.query()` returns `list[Any]`. Both are fully synchronous (no Promises/async). `from_sqlite`/`fromSqlite` is one-shot (DATA per row, then COMPLETE); compose with `switch_map` + `from_timer` for periodic re-query. `to_sqlite`/`toSqlite` follows per-record sink pattern (same as `to_postgres`/`toPostgres`). Default insert SQL uses JSON column; custom `to_sql` override available. TS uses `node:sqlite` `DatabaseSync` or `better-sqlite3`; PY uses stdlib `sqlite3` — both zero-dep from GraphReFly's perspective (user provides instance).
135
163
  - **Storage & sink adapter pattern parity (Phase 5.2d, 2026-04-04):** All 5.2d sinks follow the same pattern in both TS and PY: duck-typed client protocols, `on_message` intercepting `DATA`, `SinkTransportError` for serialize/send failures. All sinks return a `SinkHandle` with `dispose()` + `errors: Node[SinkTransportError | None]`. `dispose()` sends `TEARDOWN` to the errors node. Buffered sinks (`to_clickhouse`, `to_s3`, `to_file`, `to_csv`) return a `BufferedSinkHandle` adding `flush()`. TS `SinkHandle` has optional `flush?: ...`; PY uses separate `BufferedSinkHandle` dataclass (intentional — more Pythonic). Checkpoint adapters (`checkpoint_to_s3`, `checkpoint_to_redis`) wire `graph.auto_checkpoint()`. PY uses `threading.Timer` for flush timers; TS uses `setTimeout`. PY `to_postgres` calls `client.execute(sql, params)` (psycopg2/3 style); TS calls `client.query(sql, params)` (pg style). PY `json.dumps` includes spaces after separators; TS `JSON.stringify` does not — NDJSON output is semantically equivalent but not byte-identical across languages.
136
164
 
@@ -298,14 +326,14 @@ Cross-language: `graphrefly-ts/docs/optimizations.md` §15. **Python (shipped):*
298
326
  | **`snapshot()`** | Both: `{ version: 1, ...describe() }` — flat `version` field, sorted `nodes` keys. |
299
327
  | **`restore(data)`** | Both: validate `data.name` matches graph name; skip `derived`/`operator`/`effect` types; silently ignore unknown/failing paths. |
300
328
  | **`from_snapshot(data, build?)`** | Both: optional `build` callback registers topology before `restore()` applies values. Without `build`, both use registry-based reconstruction (mounts → topo node creation via factories → edges → restore). |
301
- | **`to_json()` / `toJSON()`** | Python returns compact JSON **string** with trailing newline. TS returns a plain sorted-key **object** (for `JSON.stringify(graph)`). Language-appropriate. |
302
- | **`toJSONString()`** | TS only — `JSON.stringify(toJSON()) + "\n"`. Python's `to_json()` serves the same role. |
329
+ | **`to_json_string()` / `toJSON()`** | Python `to_json_string()` returns compact JSON **string** with trailing newline. TS `toJSON()` returns a plain sorted-key **object** (for `JSON.stringify(graph)`). Language-appropriate. |
330
+ | **`toJSONString()`** | TS only — `JSON.stringify(toJSON()) + "\n"`. Python's `to_json_string()` serves the same role. |
303
331
 
304
332
  **Intentional divergence:**
305
333
 
306
334
  | Topic | Python | TypeScript | Rationale |
307
335
  |-------|--------|------------|-----------|
308
- | `to_json` return type | `to_json()` → `str` (no universal `__json__` hook in Python) | `toJSON()` → plain object (ECMAScript `JSON.stringify` protocol) | Language idiom |
336
+ | `to_json_string` return type | `to_json_string()` → `str` (no universal `__json__` hook in Python) | `toJSON()` → plain object (ECMAScript `JSON.stringify` protocol) | Language idiom |
309
337
  | `_parse_snapshot_envelope` | Validates `version`, `name`, `nodes`, `edges`, `subgraphs` types | Only validates `data.name` match | Python is stricter; both correct |
310
338
 
311
339
  ### Ingest adapters (roadmap 5.2c / 5.3b) — deferred items (QA)
@@ -316,7 +344,7 @@ Applies to `src/extra/adapters.ts` and `graphrefly.extra.adapters`. **Keep the t
316
344
  |------|--------|-------|
317
345
  | **`fromRedisStream` / `from_redis_stream` never emits COMPLETE** | Documented limitation (2026-04-03) | Long-lived stream consumers intentionally never complete. The consumer loop runs until teardown. This is expected behavior for persistent stream sources (same as Kafka). Document in JSDoc/docstrings. |
318
346
  | **`fromRedisStream` / `from_redis_stream` does not disconnect client** | Documented limitation (2026-04-03) | The caller owns the Redis client lifecycle. The adapter does not call `disconnect()` on teardown — the caller is responsible for closing the connection. Same contract as `fromKafka` (caller owns `consumer.connect()`/`disconnect()`). |
319
- | **PY `from_csv` / `from_ndjson` thread not joined on cleanup** | Documented limitation (2026-04-03) | Python file-ingest adapters run in a daemon thread. On teardown, `active[0] = False` signals the thread to exit but does not `join()` it. The daemon flag ensures the thread does not block process exit. A future optimization could add optional `join(timeout)` on cleanup for stricter resource control. |
347
+ | **PY `from_csv` / `from_ndjson` thread cleanup** | Resolved (2026-04-05) | `t.join(timeout=1)` added after stop flag. |
320
348
 
321
349
  ### Ingest adapters — intentional cross-language divergences (parity review 2026-04-03)
322
350
 
@@ -363,10 +391,10 @@ Applies to `src/extra/adapters.ts` and `graphrefly.extra.adapters`. **Keep the t
363
391
  | Node internals | Class-based `NodeImpl`, all methods on class | Class-based `NodeImpl`, V8 hidden class optimization, prototype methods |
364
392
  | Dep-value identity check | Before cleanup (skip cleanup+fn on no-op) | Before cleanup (skip cleanup+fn on no-op) |
365
393
  | `INVALIDATE` (§1.2) | Cleanup + clear `_cached` + `_last_dep_values`; terminal passthrough (§9); no auto recompute | Same |
366
- | `Graph` Phase 1.1 | `thread_safe` + `RLock`; TEARDOWN after unlock on `remove`; `disconnect` registry-only (§C resolved) | Registry only; `connect` / `disconnect` errors aligned; §C resolved |
394
+ | `Graph` Phase 1.1 | `thread_safe` + `RLock`; TEARDOWN after unlock on `remove`; `disconnect` registry-only (§C resolved); `add()` auto-registers edges from constructor deps | Registry only; `connect` / `disconnect` errors aligned; §C resolved; `add()` auto-registers edges from constructor deps |
367
395
  | `Graph` Phase 1.2 | Aligned: `::` path separator, mount `remove` + subtree TEARDOWN, qualified paths, `edges()`, signal mounts-first, `resolve` strips leading name, `:` in names OK; see §14 | Same; see §14 |
368
396
  | `Graph` Phase 1.3 | `describe`, `observe`, `GRAPH_META_SEGMENT`, `signal`→meta, `describe_kind` on sugar; see §15 | TS: `describe()`, `observe()`, `GRAPH_META_SEGMENT`, `describeKind` on sugar; see graphrefly-ts §15 | `observe()` order: both use full-path code-point sort (resolved 2026-03-31; see §15) |
369
- | `Graph` Phase 1.4 | `destroy`, `snapshot` (flat `version: 1`), `restore` (name check + type filter + silent catch), `from_snapshot(data, build=)`, `to_json()` → str + `\n`; see §16 | `destroy`, `snapshot`, `restore`, `fromSnapshot(data, build?)`, `toJSON()` → object, `toJSONString()` → str + `\n`; see §16 |
397
+ | `Graph` Phase 1.4 | `destroy`, `snapshot` (flat `version: 1`), `restore` (name check + type filter + silent catch), `from_snapshot(data, build=)`, `to_json_string()` → str + `\n`; see §16 | `destroy`, `snapshot`, `restore`, `fromSnapshot(data, build?)`, `toJSON()` → object, `toJSONString()` → str + `\n`; see §16 |
370
398
  | `Graph` Phase 1.5 | **Python:** `Actor`, `GuardDenied`, `policy()`, `compose_guards`, node `guard` opt, `down`/`set`/`signal`/`subscribe`/`describe` actor params, `internal` propagation bypass, `remove`/unmount subtree TEARDOWN `internal=True`; see built-in §8 | **TypeScript:** aligned — `GraphActorOptions`, `NodeTransportOptions`, scoped `describe`/`observe`, `GuardDenied.node` getter mirrors `nodeName` |
371
399
  | `policy()` semantics | Deny-overrides: any matching deny blocks; if no deny, any matching allow permits; no match → deny | Same (aligned from parity round) |
372
400
  | `DEFAULT_ACTOR` | `{"type": "system", "id": ""}` | `{ type: "system", id: "" }` (aligned) |
@@ -385,7 +413,7 @@ Applies to `src/extra/adapters.ts` and `graphrefly.extra.adapters`. **Keep the t
385
413
  | `from_event_emitter` / `fromEvent` | Generic emitter (`add_method=`, `remove_method=`) | DOM `addEventListener` API |
386
414
  | `to_array` / `toArray` | Reactive `Node[list]` | Reactive `Node<T[]>` |
387
415
  | `to_list` (blocking) | Py-only sync bridge | N/A |
388
- | Extra Phase 3.1 (resilience) | `graphrefly.extra.{backoff,resilience,checkpoint}`; see §6 below | `src/extra/{backoff,resilience,checkpoint}.ts`; see §6 below |
416
+ | Extra Phase 3.1 (resilience) | `graphrefly.extra.{backoff,resilience,checkpoint}` + `core/timer.py` (`ResettableTimer`); see §6 below | `src/extra/{backoff,resilience,checkpoint}.ts` + `core/timer.ts` (`ResettableTimer`); see §6 below |
389
417
  | Extra Phase 3.2 (data structures) | `graphrefly.extra.data_structures` (`reactive_map`, …); see §17 | `reactiveMap` + `reactive-base` (`Versioned` snapshots); see §17 |
390
418
 
391
419
  ### 18. CQRS reactive log snapshot shape (Phase 4.5 — cross-language note)
@@ -521,7 +549,7 @@ Both repos now ship a Pulsar-inspired messaging domain layer under `patterns.mes
521
549
 
522
550
  | Topic | Python | TypeScript | Rationale |
523
551
  |-------|--------|------------|-----------|
524
- | Timer base | `monotonic_ns()` (nanoseconds via `time.monotonic_ns()`) | `monotonicNs()` (nanoseconds via `performance.now()`) | Both centralised in `core/clock`; nanosecond internal tracking |
552
+ | Timer base | `monotonic_ns()` (nanoseconds via `time.monotonic_ns()`); `ResettableTimer` in `core/timer.py` | `monotonicNs()` (nanoseconds via `performance.now()`); `ResettableTimer` in `core/timer.ts` | Both centralised in `core/clock`; nanosecond internal tracking; `ResettableTimer` used by `retry`, `rate_limiter`, `timeout` |
525
553
  | Thread safety | `CircuitBreaker` + `TokenBucket` use `threading.Lock`; retry uses `threading.Timer` | Single-threaded (`setTimeout`) | Spec §6.1 |
526
554
  | `CircuitBreaker` params | `cooldown` (seconds, implicit) | `cooldownSeconds` (seconds, explicit) | Naming convention |
527
555
  | `CircuitOpenError` base | `RuntimeError` | `Error` | Language convention |
@@ -299,9 +299,9 @@ Composition layer over 3.2 (`reactive_log`), 4.1 (sagas), 4.2 (event bus), 4.3 (
299
299
 
300
300
  ### 5.2 — ORM Adapters
301
301
 
302
- - [ ] SQLAlchemy ORM integration
303
- - [ ] Django ORM integration
304
- - [ ] Tortoise ORM integration
302
+ - [x] SQLAlchemy ORM integration
303
+ - [x] Django ORM integration
304
+ - [x] Tortoise ORM integration
305
305
  - [x] `from_sqlite(db, query)` / `to_sqlite(db, table)` — SQLite via duck-typed `SqliteDbLike` (`query()` method); one-shot source + per-record sink; sync
306
306
 
307
307
  ### 5.3 — Adapters
@@ -1,6 +1,8 @@
1
1
  # GraphReFly — graphrefly (Python)
2
2
 
3
- GraphReFly is a reactive graph protocol for human + LLM co-operation. This package is the Python implementation (`graphrefly`).
3
+ Describe automations in plain language. Review them visually. Run them persistently. Trace every decision back to its source.
4
+
5
+ GraphReFly is a reactive graph engine for human + LLM co-operation. An LLM composes a reactive graph from a natural-language description (like SQL for data flows). The graph runs persistently, checkpoints state, and traces every decision through a causal chain — enabling explainability ("why was this flagged?"), auditability, and progressive trust accumulation. This package is the Python implementation (`graphrefly`). Zero dependencies.
4
6
 
5
7
  ## Authoritative behavior
6
8
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "graphrefly"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "Reactive graph protocol for human + LLM co-operation. Composable nodes, glitch-free diamond resolution, two-phase push, durable streaming. Zero dependencies."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -9,19 +9,21 @@ authors = [{ name = "David Chen" }]
9
9
  keywords = [
10
10
  "reactive",
11
11
  "graph",
12
+ "automation",
13
+ "workflow",
14
+ "llm",
15
+ "ai-agents",
16
+ "orchestration",
12
17
  "state-management",
13
18
  "signals",
14
19
  "streaming",
15
- "llm",
16
- "ai-agents",
17
20
  "observable",
18
- "orchestration",
21
+ "causal-tracing",
22
+ "human-in-the-loop",
23
+ "natural-language",
19
24
  "durable-workflow",
20
- "diamond-resolution",
21
- "zero-dependency",
22
25
  "asyncio",
23
- "callbag",
24
- "reactive-programming",
26
+ "zero-dependency",
25
27
  ]
26
28
  classifiers = [
27
29
  "Development Status :: 3 - Alpha",
@@ -15,6 +15,7 @@ from graphrefly.core import (
15
15
  Message,
16
16
  Messages,
17
17
  MessageType,
18
+ NO_VALUE,
18
19
  Node,
19
20
  NodeActions,
20
21
  NodeFn,
@@ -115,6 +116,7 @@ __all__ = [
115
116
  "Message",
116
117
  "MessageType",
117
118
  "Messages",
119
+ "NO_VALUE",
118
120
  "Node",
119
121
  "PipeOperator",
120
122
  "NodeActions",
@@ -18,6 +18,7 @@ from graphrefly.core.guard import (
18
18
  )
19
19
  from graphrefly.core.meta import describe_node, meta_snapshot
20
20
  from graphrefly.core.node import (
21
+ NO_VALUE,
21
22
  Node,
22
23
  NodeActions,
23
24
  NodeFn,
@@ -64,6 +65,7 @@ from graphrefly.core.sugar import (
64
65
  producer,
65
66
  state,
66
67
  )
68
+ from graphrefly.core.timer import ResettableTimer
67
69
  from graphrefly.core.versioning import (
68
70
  V0,
69
71
  V1,
@@ -87,6 +89,7 @@ __all__ = [
87
89
  "GuardDenied",
88
90
  "GuardFn",
89
91
  "Message",
92
+ "NO_VALUE",
90
93
  "MessageType",
91
94
  "Messages",
92
95
  "Node",
@@ -95,6 +98,7 @@ __all__ = [
95
98
  "NodeFn",
96
99
  "NodeImpl",
97
100
  "NodeStatus",
101
+ "ResettableTimer",
98
102
  "SubscribeHints",
99
103
  "monotonic_ns",
100
104
  "wall_clock_ns",
@@ -15,6 +15,7 @@ from contextlib import suppress
15
15
  from types import MappingProxyType
16
16
  from typing import Any
17
17
 
18
+ from graphrefly.core.node import _SENTINEL
18
19
  from graphrefly.core.protocol import Messages, MessageType, emit_with_batch, propagates_to_meta
19
20
 
20
21
  # ---------------------------------------------------------------------------
@@ -189,7 +190,7 @@ class DynamicNodeImpl[T]:
189
190
  self._thread_safe = bool(thread_safe)
190
191
  self._inspector_hook: Callable[[dict[str, Any]], None] | None = None
191
192
 
192
- self._cached: T | None = None
193
+ self._cached: Any = _SENTINEL
193
194
  self._status: str = "disconnected"
194
195
  self._terminal = False
195
196
  self._connected = False
@@ -277,8 +278,10 @@ class DynamicNodeImpl[T]:
277
278
  lock = self._cache_lock
278
279
  if lock is not None:
279
280
  with lock:
280
- return self._cached
281
- return self._cached
281
+ v = self._cached
282
+ else:
283
+ v = self._cached
284
+ return None if v is _SENTINEL else v
282
285
 
283
286
  def down(
284
287
  self,
@@ -346,6 +349,12 @@ class DynamicNodeImpl[T]:
346
349
  ) -> Callable[[], None]:
347
350
  if self._terminal and self._resubscribable:
348
351
  self._terminal = False
352
+ lock = self._cache_lock
353
+ if lock is not None:
354
+ with lock:
355
+ self._cached = _SENTINEL
356
+ else:
357
+ self._cached = _SENTINEL
349
358
  self._status = "disconnected"
350
359
  if self._on_resubscribe is not None:
351
360
  self._on_resubscribe()
@@ -519,11 +528,14 @@ class DynamicNodeImpl[T]:
519
528
  if t is MessageType.INVALIDATE:
520
529
  if lock is not None:
521
530
  with lock:
522
- self._cached = None
531
+ self._cached = _SENTINEL
523
532
  else:
524
- self._cached = None
525
- if t is MessageType.DATA or t is MessageType.RESOLVED:
533
+ self._cached = _SENTINEL
534
+ self._status = "dirty"
535
+ if t is MessageType.DATA:
526
536
  self._status = "settled"
537
+ elif t is MessageType.RESOLVED:
538
+ self._status = "resolved"
527
539
  elif t is MessageType.DIRTY:
528
540
  self._status = "dirty"
529
541
  elif t is MessageType.COMPLETE:
@@ -536,9 +548,9 @@ class DynamicNodeImpl[T]:
536
548
  if self._reset_on_teardown:
537
549
  if lock is not None:
538
550
  with lock:
539
- self._cached = None
551
+ self._cached = _SENTINEL
540
552
  else:
541
- self._cached = None
553
+ self._cached = _SENTINEL
542
554
  try:
543
555
  self._propagate_to_meta(t)
544
556
  finally:
@@ -561,7 +573,13 @@ class DynamicNodeImpl[T]:
561
573
  cached_snapshot = self._cached
562
574
  else:
563
575
  cached_snapshot = self._cached
564
- unchanged = self._equals(cached_snapshot, value)
576
+ try:
577
+ unchanged = cached_snapshot is not _SENTINEL and self._equals(cached_snapshot, value)
578
+ except Exception as eq_err:
579
+ wrapped = RuntimeError(f'Node "{self._name}": equals threw: {eq_err}')
580
+ wrapped.__cause__ = eq_err
581
+ self._down_internal([(MessageType.ERROR, wrapped)])
582
+ return
565
583
  if unchanged:
566
584
  msgs: Messages = (
567
585
  [(MessageType.RESOLVED,)]
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable, Mapping
6
+ from dataclasses import dataclass
6
7
  from typing import Any, Literal, TypedDict
7
8
 
8
9
  type GuardAction = str
@@ -255,11 +256,19 @@ def access_hint_for_guard(guard: GuardFn) -> str:
255
256
  return "+".join(allowed)
256
257
 
257
258
 
258
- def record_mutation(actor: Mapping[str, Any]) -> dict[str, Any]:
259
+ @dataclass(frozen=True, slots=True)
260
+ class MutationRecord:
261
+ """Snapshot for :attr:`~graphrefly.core.node.NodeImpl.last_mutation`."""
262
+
263
+ actor: dict[str, Any]
264
+ timestamp_ns: int
265
+
266
+
267
+ def record_mutation(actor: Mapping[str, Any]) -> MutationRecord:
259
268
  """Snapshot for :attr:`~graphrefly.core.node.NodeImpl.last_mutation`."""
260
269
  from graphrefly.core.clock import wall_clock_ns
261
270
 
262
- return {"actor": dict(normalize_actor(actor)), "timestamp_ns": wall_clock_ns()}
271
+ return MutationRecord(actor=dict(normalize_actor(actor)), timestamp_ns=wall_clock_ns())
263
272
 
264
273
 
265
274
  __all__ = [
@@ -267,6 +276,7 @@ __all__ = [
267
276
  "GuardAction",
268
277
  "GuardDenied",
269
278
  "GuardFn",
279
+ "MutationRecord",
270
280
  "access_hint_for_guard",
271
281
  "compose_guards",
272
282
  "normalize_actor",