graphrefly 0.7.0__tar.gz → 0.9.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 (294) hide show
  1. {graphrefly-0.7.0 → graphrefly-0.9.0}/CHANGELOG.md +16 -0
  2. {graphrefly-0.7.0 → graphrefly-0.9.0}/PKG-INFO +1 -1
  3. {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/optimizations.md +4 -0
  4. {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/roadmap.md +16 -12
  5. {graphrefly-0.7.0 → graphrefly-0.9.0}/pyproject.toml +2 -1
  6. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/compat/trio_runner.py +1 -1
  7. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/dynamic_node.py +14 -7
  8. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/node.py +23 -12
  9. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/protocol.py +1 -1
  10. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/timer.py +5 -1
  11. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/adapters.py +18 -21
  12. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/cascading_cache.py +17 -10
  13. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/resilience.py +4 -3
  14. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/graph/graph.py +14 -7
  15. graphrefly-0.9.0/src/graphrefly/integrations/django.py +584 -0
  16. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/integrations/fastapi.py +45 -17
  17. graphrefly-0.9.0/src/graphrefly/patterns/__init__.py +23 -0
  18. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/ai.py +2 -2
  19. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/cqrs.py +29 -26
  20. graphrefly-0.9.0/src/graphrefly/patterns/graphspec.py +1212 -0
  21. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/reactive_layout/measurement_adapters.py +1 -1
  22. graphrefly-0.9.0/src/graphrefly/patterns/reduction.py +656 -0
  23. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_core.py +1 -1
  24. graphrefly-0.9.0/tests/test_django.py +436 -0
  25. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_fastapi.py +5 -3
  26. graphrefly-0.9.0/tests/test_graphspec.py +612 -0
  27. graphrefly-0.9.0/tests/test_patterns_reduction.py +419 -0
  28. graphrefly-0.9.0/tests/test_reduction.py +421 -0
  29. graphrefly-0.7.0/src/graphrefly/patterns/__init__.py +0 -5
  30. {graphrefly-0.7.0 → graphrefly-0.9.0}/.claude/skills/dev-dispatch/SKILL.md +0 -0
  31. {graphrefly-0.7.0 → graphrefly-0.9.0}/.claude/skills/parity/SKILL.md +0 -0
  32. {graphrefly-0.7.0 → graphrefly-0.9.0}/.claude/skills/qa/SKILL.md +0 -0
  33. {graphrefly-0.7.0 → graphrefly-0.9.0}/.gemini/skills/dev-dispatch/SKILL.md +0 -0
  34. {graphrefly-0.7.0 → graphrefly-0.9.0}/.gemini/skills/parity/SKILL.md +0 -0
  35. {graphrefly-0.7.0 → graphrefly-0.9.0}/.github/workflows/pages.yml +0 -0
  36. {graphrefly-0.7.0 → graphrefly-0.9.0}/.github/workflows/release.yml +0 -0
  37. {graphrefly-0.7.0 → graphrefly-0.9.0}/.gitignore +0 -0
  38. {graphrefly-0.7.0 → graphrefly-0.9.0}/.mise.toml +0 -0
  39. {graphrefly-0.7.0 → graphrefly-0.9.0}/CLAUDE.md +0 -0
  40. {graphrefly-0.7.0 → graphrefly-0.9.0}/CONTRIBUTING.md +0 -0
  41. {graphrefly-0.7.0 → graphrefly-0.9.0}/GEMINI.md +0 -0
  42. {graphrefly-0.7.0 → graphrefly-0.9.0}/LICENSE +0 -0
  43. {graphrefly-0.7.0 → graphrefly-0.9.0}/README.md +0 -0
  44. {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/DESIGN-ARCHIVE-INDEX.md +0 -0
  45. {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-access-control-actor-guard.md +0 -0
  46. {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-cross-repo-implementation-audit.md +0 -0
  47. {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-demo-test-strategy.md +0 -0
  48. {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-graphrefly-spec-design.md +0 -0
  49. {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-serialization-memory-footprint.md +0 -0
  50. {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-tier2-parity-nonlocal-forward-inner.md +0 -0
  51. {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-universal-reduction-layer.md +0 -0
  52. {graphrefly-0.7.0 → graphrefly-0.9.0}/benchmarks/py-baseline.json +0 -0
  53. {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/ADAPTER-CONTRACT.md +0 -0
  54. {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/benchmark.md +0 -0
  55. {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/docs-guidance.md +0 -0
  56. {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/test-guidance.md +0 -0
  57. {graphrefly-0.7.0 → graphrefly-0.9.0}/examples/README.md +0 -0
  58. {graphrefly-0.7.0 → graphrefly-0.9.0}/examples/basic_counter.py +0 -0
  59. {graphrefly-0.7.0 → graphrefly-0.9.0}/llms.txt +0 -0
  60. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/__init__.py +1 -1
  61. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/compat/__init__.py +0 -0
  62. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/compat/async_utils.py +0 -0
  63. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/compat/asyncio_runner.py +0 -0
  64. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/__init__.py +0 -0
  65. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/cancellation.py +0 -0
  66. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/clock.py +0 -0
  67. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/guard.py +0 -0
  68. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/meta.py +0 -0
  69. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/runner.py +0 -0
  70. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/subgraph_locks.py +0 -0
  71. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/sugar.py +0 -0
  72. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/versioning.py +0 -0
  73. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/__init__.py +0 -0
  74. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/backoff.py +0 -0
  75. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/backpressure.py +0 -0
  76. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/checkpoint.py +0 -0
  77. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/composite.py +0 -0
  78. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/cron.py +0 -0
  79. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/data_structures.py +0 -0
  80. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/sources.py +0 -0
  81. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/tier1.py +0 -0
  82. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/tier2.py +0 -0
  83. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/graph/__init__.py +0 -0
  84. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/integrations/__init__.py +0 -0
  85. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/memory.py +0 -0
  86. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/messaging.py +0 -0
  87. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/orchestration.py +0 -0
  88. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/reactive_layout/__init__.py +0 -0
  89. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/reactive_layout/reactive_block_layout.py +0 -0
  90. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/reactive_layout/reactive_layout.py +0 -0
  91. {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/py.typed +0 -0
  92. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/bench_core.py +0 -0
  93. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/conftest.py +0 -0
  94. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_adapter_contract.py +0 -0
  95. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_adapters_ingest.py +0 -0
  96. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_adapters_storage.py +0 -0
  97. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_backpressure.py +0 -0
  98. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_cascading_cache.py +0 -0
  99. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_concurrency.py +0 -0
  100. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_dynamic_node.py +0 -0
  101. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_edge_cases.py +0 -0
  102. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_composite.py +0 -0
  103. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_data_structures.py +0 -0
  104. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_resilience.py +0 -0
  105. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_sources.py +0 -0
  106. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_sources_http.py +0 -0
  107. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_tier1.py +0 -0
  108. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_tier2.py +0 -0
  109. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_graph.py +0 -0
  110. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_guard.py +0 -0
  111. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_measurement_adapters.py +0 -0
  112. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_ai.py +0 -0
  113. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_cqrs.py +0 -0
  114. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_memory.py +0 -0
  115. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_messaging.py +0 -0
  116. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_orchestration.py +0 -0
  117. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_perf_smoke.py +0 -0
  118. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_protocol.py +0 -0
  119. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_reactive_block_layout.py +0 -0
  120. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_reactive_layout.py +0 -0
  121. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_regressions.py +0 -0
  122. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_runner.py +0 -0
  123. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_smoke.py +0 -0
  124. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_sugar.py +0 -0
  125. {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_versioning.py +0 -0
  126. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/.gitignore +0 -0
  127. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/README.md +0 -0
  128. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/astro.config.mjs +0 -0
  129. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/content.config.ts +0 -0
  130. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/package.json +0 -0
  131. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/pnpm-lock.yaml +0 -0
  132. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/public/llms.txt +0 -0
  133. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/py-api-sidebar.mjs +0 -0
  134. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/scripts/gen_api_docs.py +0 -0
  135. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/scripts/sync-docs.mjs +0 -0
  136. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/GraphreflyHero.astro +0 -0
  137. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/Header.astro +0 -0
  138. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/MobileMenuFooter.astro +0 -0
  139. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/PyodidePlayground.tsx +0 -0
  140. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/Sidebar.astro +0 -0
  141. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/SiteTitle.astro +0 -0
  142. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/BackoffPreset.md +0 -0
  143. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/BackoffStrategy.md +0 -0
  144. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/CheckpointAdapter.md +0 -0
  145. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/CircuitBreaker.md +0 -0
  146. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/CircuitOpenError.md +0 -0
  147. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/DeferWhen.md +0 -0
  148. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/DictCheckpointAdapter.md +0 -0
  149. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/DistillBundle.md +0 -0
  150. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/EmitStrategy.md +0 -0
  151. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/Extraction.md +0 -0
  152. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/FileCheckpointAdapter.md +0 -0
  153. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/HttpBundle.md +0 -0
  154. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/JitterMode.md +0 -0
  155. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/MemoryCheckpointAdapter.md +0 -0
  156. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/Message.md +0 -0
  157. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/MessageType.md +0 -0
  158. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/Messages.md +0 -0
  159. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/NodeActions.md +0 -0
  160. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/NodeFn.md +0 -0
  161. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/NodeImpl.md +0 -0
  162. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/NodeStatus.md +0 -0
  163. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/PipeOperator.md +0 -0
  164. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/PubSubHub.md +0 -0
  165. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/ReactiveIndexBundle.md +0 -0
  166. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/ReactiveListBundle.md +0 -0
  167. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/ReactiveLogBundle.md +0 -0
  168. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/ReactiveMapBundle.md +0 -0
  169. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/SqliteCheckpointAdapter.md +0 -0
  170. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/SubscribeHints.md +0 -0
  171. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/TokenBucket.md +0 -0
  172. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/VerifiableBundle.md +0 -0
  173. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/Versioned.md +0 -0
  174. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/WithBreakerBundle.md +0 -0
  175. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/WithStatusBundle.md +0 -0
  176. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/audit.md +0 -0
  177. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/batch.md +0 -0
  178. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/buffer.md +0 -0
  179. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/buffer_count.md +0 -0
  180. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/buffer_time.md +0 -0
  181. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/cached.md +0 -0
  182. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/checkpoint_node_value.md +0 -0
  183. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/circuit_breaker.md +0 -0
  184. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/combine.md +0 -0
  185. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/concat.md +0 -0
  186. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/concat_map.md +0 -0
  187. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/constant.md +0 -0
  188. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/debounce.md +0 -0
  189. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/decorrelated_jitter.md +0 -0
  190. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/delay.md +0 -0
  191. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/derived.md +0 -0
  192. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/dispatch_messages.md +0 -0
  193. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/distill.md +0 -0
  194. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/distinct_until_changed.md +0 -0
  195. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/effect.md +0 -0
  196. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/element_at.md +0 -0
  197. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/emit_with_batch.md +0 -0
  198. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/empty.md +0 -0
  199. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/exhaust_map.md +0 -0
  200. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/exponential.md +0 -0
  201. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/fibonacci.md +0 -0
  202. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/filter.md +0 -0
  203. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/find.md +0 -0
  204. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/first.md +0 -0
  205. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/first_value_from.md +0 -0
  206. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/flat_map.md +0 -0
  207. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/for_each.md +0 -0
  208. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_any.md +0 -0
  209. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_async_iter.md +0 -0
  210. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_awaitable.md +0 -0
  211. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_cron.md +0 -0
  212. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_event_emitter.md +0 -0
  213. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_fs_watch.md +0 -0
  214. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_git_hook.md +0 -0
  215. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_http.md +0 -0
  216. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_iter.md +0 -0
  217. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_mcp.md +0 -0
  218. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_timer.md +0 -0
  219. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_webhook.md +0 -0
  220. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_websocket.md +0 -0
  221. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/gate.md +0 -0
  222. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/index.md +0 -0
  223. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/interval.md +0 -0
  224. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/is_batching.md +0 -0
  225. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/is_phase2_message.md +0 -0
  226. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/is_terminal_message.md +0 -0
  227. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/last.md +0 -0
  228. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/linear.md +0 -0
  229. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/log_slice.md +0 -0
  230. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/map.md +0 -0
  231. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/merge.md +0 -0
  232. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/message_tier.md +0 -0
  233. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/never.md +0 -0
  234. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/node.md +0 -0
  235. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/of.md +0 -0
  236. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/operator.md +0 -0
  237. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/pairwise.md +0 -0
  238. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/partition_for_batch.md +0 -0
  239. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/pausable.md +0 -0
  240. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/pipe.md +0 -0
  241. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/producer.md +0 -0
  242. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/propagates_to_meta.md +0 -0
  243. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/pubsub.md +0 -0
  244. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/race.md +0 -0
  245. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/rate_limiter.md +0 -0
  246. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reactive_index.md +0 -0
  247. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reactive_list.md +0 -0
  248. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reactive_log.md +0 -0
  249. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reactive_map.md +0 -0
  250. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reduce.md +0 -0
  251. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/repeat.md +0 -0
  252. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/replay.md +0 -0
  253. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/rescue.md +0 -0
  254. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/resolve_backoff_preset.md +0 -0
  255. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/restore_graph_checkpoint.md +0 -0
  256. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/retry.md +0 -0
  257. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/sample.md +0 -0
  258. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/save_graph_checkpoint.md +0 -0
  259. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/scan.md +0 -0
  260. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/share.md +0 -0
  261. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/skip.md +0 -0
  262. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/start_with.md +0 -0
  263. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/state.md +0 -0
  264. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/subscribe.md +0 -0
  265. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/switch_map.md +0 -0
  266. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/take.md +0 -0
  267. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/take_until.md +0 -0
  268. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/take_while.md +0 -0
  269. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/tap.md +0 -0
  270. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/throttle.md +0 -0
  271. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/throw_error.md +0 -0
  272. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/timeout.md +0 -0
  273. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/to_array.md +0 -0
  274. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/to_list.md +0 -0
  275. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/to_sse.md +0 -0
  276. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/to_websocket.md +0 -0
  277. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/token_bucket.md +0 -0
  278. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/token_tracker.md +0 -0
  279. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/verifiable.md +0 -0
  280. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/window.md +0 -0
  281. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/window_count.md +0 -0
  282. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/window_time.md +0 -0
  283. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/with_breaker.md +0 -0
  284. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/with_latest_from.md +0 -0
  285. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/with_max_attempts.md +0 -0
  286. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/with_status.md +0 -0
  287. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/zip.md +0 -0
  288. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/index.mdx +0 -0
  289. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/lab/python.mdx +0 -0
  290. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content.config.ts +0 -0
  291. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/env.d.ts +0 -0
  292. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/styles/custom.css +0 -0
  293. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/theme-prototypes.html +0 -0
  294. {graphrefly-0.7.0 → graphrefly-0.9.0}/website/tsconfig.json +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.9.0 (2026-04-06)
6
+
7
+ ### Features
8
+
9
+ - 8.3
10
+ ([`39b0a9e`](https://github.com/graphrefly/graphrefly-py/commit/39b0a9e8e2a034a27352b2ea054182d58065903f))
11
+
12
+
13
+ ## v0.8.0 (2026-04-06)
14
+
15
+ ### Features
16
+
17
+ - 8.1 + django integration
18
+ ([`5dcfd3d`](https://github.com/graphrefly/graphrefly-py/commit/5dcfd3d78561e7ae8cdc905f95f9b4922926881e))
19
+
20
+
5
21
  ## v0.7.0 (2026-04-06)
6
22
 
7
23
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphrefly
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -139,6 +139,10 @@ Union-find over node identity merges components when nodes list dependencies at
139
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
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
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.
142
+ - **GraphSpec cross-language parity (Phase 8.3, noted 2026-04-06, QA 2026-04-06):** Both TS and PY implement `compileSpec`/`compile_spec`, `decompileGraph`/`decompile_graph`, `llmCompose`/`llm_compose`, `llmRefine`/`llm_refine`, `specDiff`/`spec_diff`, and `validateSpec`/`validate_spec` in `patterns/graphspec`. Key alignment: (1) `GraphSpec` schema is identical JSON shape — `nodes`, `templates`, `feedback` top-level keys. TS uses TypeScript types; PY uses TypedDict. (2) `compile_spec` resolves nodes in dependency order (state/producer first, then derived/effect/operator). Catalog is passed explicitly (`GraphSpecCatalog`) — no global registry. (3) Template instantiation creates mounted subgraphs via `graph.mount()`. `$param` bindings resolve to top-level nodes. (4) Feedback edges wire via §8.1 `feedback()` primitive. (5) `decompile_graph` uses `describe(detail="standard")`, skips `__meta__` and `__feedback_*` internal nodes. Template detection via meta-based recovery (primary) + structural fingerprinting (fallback; includes dep names for accuracy). (6) `spec_diff` is pure JSON comparison — template-aware, feedback-aware. (7) LLM APIs (`llm_compose`/`llm_refine`) share identical system prompt and validation pipeline. (8) `validate_spec` checks bind targets exist in outer nodes, rejects feedback self-cycles, validates template param completeness. QA fixes: idempotent unsub in feedback(), deterministic output node selection in decompile, `contextlib.suppress` for connect dedup. Both repos: 35 tests each.
143
+ - **Feedback bare DATA to reentry/counter — deferred to 8.2 (Phase 8.1, noted 2026-04-06, decided 2026-04-06):** Both TS and PY `feedback()` send bare `[DATA]` to `reentry` and `counter` nodes without a preceding `DIRTY`. This is a deliberate protocol shortcut: the feedback subscriber operates outside the normal two-phase push because it is a subscribe-based bridge (not a node). Acceptable because feedback reentry targets are always state nodes (which re-derive DIRTY internally via `down()`). Will be resolved when feedback is rearchitected as a graph-visible bridge node in 8.2 — at that point the bridge node participates in two-phase push naturally.
144
+ - **`llm_compose`/`llm_refine` sync (PY) vs async (TS) — intentional divergence (Phase 8.3, noted 2026-04-06):** PY: synchronous, adapter must return `LLMResponse` directly. TS: `async function` returning `Promise<GraphSpec>`. PY design invariant: no `async def` / `Awaitable` in public APIs. TS spec §5.10 allows `await` at system boundaries (LLM adapter is external I/O, not reactive scheduling). Both are correct for their language idiom. Adapter contracts differ: PY adapters must be sync; TS adapters return Promise-compatible values.
145
+ - **Reduction primitives cross-language parity (Phase 8.1, noted 2026-04-06, QA 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` bridges stages via `subscribe` forwarding DIRTY/DATA/RESOLVED/COMPLETE/ERROR to preserve two-phase protocol. TODO(8.2): replace with graph-visible bridge nodes. (3) `feedback` counter node is source of truth (resettable via `graph.set()`); uses `continue` (not `return`) on max_iterations so remaining batch messages process. 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.
142
146
 
143
147
  ---
144
148
 
@@ -293,7 +293,7 @@ Composition layer over 3.2 (`reactive_log`), 4.1 (sagas), 4.2 (event bus), 4.3 (
293
293
  ### 5.1 — Framework compat
294
294
 
295
295
  - [x] FastAPI integration
296
- - [ ] Django integration
296
+ - [x] Django integration
297
297
  - [x] asyncio / trio Runner protocol
298
298
  - [x] Async utilities: `to_async_iter`, `first_value_from_async`, `settled`
299
299
 
@@ -487,11 +487,11 @@ Reusable patterns for taking heterogeneous massive inputs and producing prioriti
487
487
 
488
488
  Composable building blocks between sources and sinks.
489
489
 
490
- - [ ] `stratify(source, rules)` → Graph — route input to different reduction branches based on classifier fn. Each branch gets independent operator chains. Rules are reactive — an LLM can rewrite them at runtime.
491
- - [ ] `funnel(sources, stages)` → Graph — multi-source merge with sequential reduction stages. Each stage is a named subgraph. Stages are pluggable — swap a stage by graph composition.
492
- - [ ] `feedback(graph, condition, reentry)` → Graph — introduce a cycle: when condition node fires, route output back to reentry point. Bounded by max iterations + budget constraints.
493
- - [ ] `budget_gate(source, constraints)` → Node — pass-through respecting reactive constraint nodes (token budget, network IO, cost ceiling). Backpressure via PAUSE/RESUME.
494
- - [ ] `scorer(sources, weights)` → Node — reactive multi-signal scoring. Weights are nodes (LLM or human can adjust live). Output: sorted, prioritized items with full score breakdown in meta.
490
+ - [x] `stratify(source, rules)` → Graph — route input to different reduction branches based on classifier fn. Each branch gets independent operator chains. Rules are reactive — an LLM can rewrite them at runtime.
491
+ - [x] `funnel(sources, stages)` → Graph — multi-source merge with sequential reduction stages. Each stage is a named subgraph. Stages are pluggable — swap a stage by graph composition.
492
+ - [x] `feedback(graph, condition, reentry)` → Graph — introduce a cycle: when condition node fires, route output back to reentry point. Bounded by max iterations + budget constraints.
493
+ - [x] `budget_gate(source, constraints)` → Node — pass-through respecting reactive constraint nodes (token budget, network IO, cost ceiling). Backpressure via PAUSE/RESUME.
494
+ - [x] `scorer(sources, weights)` → Node — reactive multi-signal scoring. Weights are nodes (LLM or human can adjust live). Output: sorted, prioritized items with full score breakdown in meta.
495
495
 
496
496
  ### 8.2 — Domain templates (opinionated Graph factories)
497
497
 
@@ -501,15 +501,19 @@ Pre-wired graphs for common "info → action" domains. Users fork/extend.
501
501
  - [ ] `issue_tracker_graph(opts)` → Graph — findings → extraction → verifiable assertions → regression detection → distillation → prioritized queue
502
502
  - [ ] `content_moderation_graph(opts)` → Graph — ingest → LLM classification → human review → feedback → policy refinement
503
503
  - [ ] `data_quality_graph(opts)` → Graph — DB/API ingest → schema validation → anomaly detection → drift alerting → remediation suggestions
504
+ - [ ] 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)
505
+ - [ ] Rearchitect `funnel()` bridges as graph-visible nodes (replaces subscribe forwarding; resolves §5.9 imperative trigger violation + teardown leak)
506
+ - [ ] `stratify` two-dep gating: gate classification on both source and rules settling (eliminates stale-rules race when both updated in same `batch()`)
504
507
 
505
508
  ### 8.3 — LLM graph composition
506
509
 
507
- - [ ] `GraphSpec` schema — JSON schema for declarative graph topology. Serializable, diffable.
508
- - [ ] `compile_spec(spec)` → Graph — instantiate from spec
509
- - [ ] `decompile_graph(graph)` → GraphSpec — extract spec from running graph
510
- - [ ] `llm_compose(problem, adapter, opts)` → GraphSpec — LLM generates topology from natural language
511
- - [ ] `llm_refine(graph, feedback, adapter)` → GraphSpec — LLM modifies existing topology
512
- - [ ] `spec_diff(spec_a, spec_b)` — structural diff between specs
510
+ - [x] `GraphSpec` schema — JSON schema for declarative graph topology. Serializable, diffable.
511
+ - [x] `compile_spec(spec, catalog)` → Graph — instantiate from spec (dep-order resolution, templates via `mount()`, feedback via §8.1)
512
+ - [x] `decompile_graph(graph)` → GraphSpec — extract spec from running graph (meta-based + structural fingerprint template detection)
513
+ - [x] `llm_compose(problem, adapter, opts)` → GraphSpec — LLM generates topology from natural language
514
+ - [x] `llm_refine(spec, feedback, adapter)` → GraphSpec — LLM modifies existing topology
515
+ - [x] `spec_diff(spec_a, spec_b)` — structural diff between specs (template-aware, feedback-aware)
516
+ - [x] `validate_spec(spec)` → GraphSpecValidation — structural validation (types, deps, templates, feedback, bind targets)
513
517
 
514
518
  ### 8.4 — Audit & accountability
515
519
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "graphrefly"
3
- version = "0.7.0"
3
+ version = "0.9.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"
@@ -60,6 +60,7 @@ dev = [
60
60
  "ruff>=0.9",
61
61
  "mypy>=1.14",
62
62
  "croniter>=2.0",
63
+ "django>=4.2",
63
64
  "fastapi>=0.100",
64
65
  "httpx>=0.24",
65
66
  "pillow>=12.2.0",
@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Any
24
24
  if TYPE_CHECKING:
25
25
  from collections.abc import Callable, Coroutine
26
26
 
27
- import trio # type: ignore[import-not-found]
27
+ import trio
28
28
 
29
29
 
30
30
  class TrioRunner:
@@ -13,10 +13,19 @@ import threading
13
13
  from collections.abc import Callable, Mapping
14
14
  from contextlib import suppress
15
15
  from types import MappingProxyType
16
- from typing import Any
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from graphrefly.core.guard import MutationRecord
17
20
 
18
21
  from graphrefly.core.node import _SENTINEL
19
- from graphrefly.core.protocol import Messages, MessageType, emit_with_batch, propagates_to_meta
22
+ from graphrefly.core.protocol import (
23
+ Messages,
24
+ MessageType,
25
+ emit_with_batch,
26
+ message_tier,
27
+ propagates_to_meta,
28
+ )
20
29
 
21
30
  # ---------------------------------------------------------------------------
22
31
  # Public types
@@ -184,7 +193,7 @@ class DynamicNodeImpl[T]:
184
193
  self._on_resubscribe = on_resubscribe
185
194
  self._auto_complete = complete_when_deps_complete
186
195
  self._describe_kind = describe_kind
187
- self._last_mutation: dict[str, Any] | None = None
196
+ self._last_mutation: MutationRecord | None = None
188
197
  self._resubscribable = resubscribable
189
198
  self._reset_on_teardown = reset_on_teardown
190
199
  self._thread_safe = bool(thread_safe)
@@ -255,7 +264,7 @@ class DynamicNodeImpl[T]:
255
264
  return self._meta
256
265
 
257
266
  @property
258
- def last_mutation(self) -> dict[str, Any] | None:
267
+ def last_mutation(self) -> MutationRecord | None:
259
268
  return self._last_mutation
260
269
 
261
270
  @property
@@ -503,9 +512,7 @@ class DynamicNodeImpl[T]:
503
512
 
504
513
  # singleDep DIRTY skip optimization
505
514
  if self._can_skip_dirty():
506
- has_phase2 = any(
507
- m[0] is MessageType.DATA or m[0] is MessageType.RESOLVED for m in messages
508
- )
515
+ has_phase2 = any(message_tier(m[0]) == 2 for m in messages)
509
516
  if has_phase2:
510
517
  filtered = [m for m in messages if m[0] is not MessageType.DIRTY]
511
518
  if filtered:
@@ -14,10 +14,17 @@ from graphrefly.core.guard import (
14
14
  Actor,
15
15
  GuardAction,
16
16
  GuardDenied,
17
+ MutationRecord,
17
18
  normalize_actor,
18
19
  record_mutation,
19
20
  )
20
- from graphrefly.core.protocol import Messages, MessageType, emit_with_batch, propagates_to_meta
21
+ from graphrefly.core.protocol import (
22
+ Messages,
23
+ MessageType,
24
+ emit_with_batch,
25
+ message_tier,
26
+ propagates_to_meta,
27
+ )
21
28
  from graphrefly.core.subgraph_locks import (
22
29
  acquire_subgraph_write_lock_with_defer,
23
30
  ensure_registered,
@@ -253,10 +260,10 @@ class NodeImpl[T]:
253
260
  msg = "node option 'guard' must be callable or None"
254
261
  raise TypeError(msg)
255
262
  self._guard: Callable[[Actor, GuardAction], bool] | None = raw_guard
256
- self._last_mutation: dict[str, Any] | None = None
263
+ self._last_mutation: MutationRecord | None = None
257
264
 
258
265
  self._cache_lock = threading.Lock() if self._thread_safe else None
259
- self._cached: Any = opts["initial"] if "initial" in opts else _SENTINEL
266
+ self._cached: Any = opts.get("initial", _SENTINEL)
260
267
  self._status: NodeStatus = "disconnected" if self._has_deps else "settled"
261
268
 
262
269
  # Versioning (GRAPHREFLY-SPEC §7)
@@ -367,7 +374,7 @@ class NodeImpl[T]:
367
374
  else:
368
375
  self._cached = m[1] # type: ignore[misc]
369
376
  if self._versioning is not None:
370
- advance_version(self._versioning, m[1], self._hash_fn)
377
+ advance_version(self._versioning, m[1], self._hash_fn) # type: ignore[misc]
371
378
  if t is MessageType.INVALIDATE:
372
379
  # GRAPHREFLY-SPEC §1.2: clear cached state; do not auto-emit from here.
373
380
  if self._cleanup is not None:
@@ -761,7 +768,7 @@ class NodeImpl[T]:
761
768
  return None if v is _SENTINEL else v
762
769
 
763
770
  @property
764
- def last_mutation(self) -> dict[str, Any] | None:
771
+ def last_mutation(self) -> MutationRecord | None:
765
772
  """Last non-internal ``write`` attribution (``actor``, ``timestamp_ns``), if any."""
766
773
  return self._last_mutation
767
774
 
@@ -823,12 +830,7 @@ class NodeImpl[T]:
823
830
  sink_messages = terminal_passthrough
824
831
  self._handle_local_lifecycle(lifecycle_messages)
825
832
  if self._can_skip_dirty():
826
- has_phase2 = False
827
- for m in sink_messages:
828
- t = m[0]
829
- if t is MessageType.DATA or t is MessageType.RESOLVED:
830
- has_phase2 = True
831
- break
833
+ has_phase2 = any(message_tier(m[0]) == 2 for m in sink_messages)
832
834
  if has_phase2:
833
835
  filtered = [m for m in sink_messages if m[0] is not MessageType.DIRTY]
834
836
  if filtered:
@@ -991,4 +993,13 @@ def node(
991
993
  # Public alias for type hints
992
994
  Node = NodeImpl
993
995
 
994
- __all__ = ["NO_VALUE", "Node", "NodeActions", "NodeFn", "NodeImpl", "NodeStatus", "SubscribeHints", "node"]
996
+ __all__ = [
997
+ "NO_VALUE",
998
+ "Node",
999
+ "NodeActions",
1000
+ "NodeFn",
1001
+ "NodeImpl",
1002
+ "NodeStatus",
1003
+ "SubscribeHints",
1004
+ "node",
1005
+ ]
@@ -359,7 +359,7 @@ def _emit_partition(
359
359
  # skip partition_for_batch allocation entirely.
360
360
  if len(messages) == 1:
361
361
  t = messages[0][0]
362
- if t is MessageType.DATA or t is MessageType.RESOLVED:
362
+ if message_tier(t) == 2:
363
363
  if _should_defer_phase2(bs, defer_when):
364
364
 
365
365
  def _emit_single() -> None:
@@ -5,6 +5,10 @@ from_timer creates a new Node per reset)."""
5
5
  from __future__ import annotations
6
6
 
7
7
  import threading
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
8
12
 
9
13
 
10
14
  class ResettableTimer:
@@ -16,7 +20,7 @@ class ResettableTimer:
16
20
  self._timer: threading.Timer | None = None
17
21
  self._lock = threading.Lock()
18
22
 
19
- def start(self, delay_seconds: float, callback: callable) -> None:
23
+ def start(self, delay_seconds: float, callback: Callable[[], Any]) -> None:
20
24
  """Schedule callback after delay_seconds. Cancels any pending timer."""
21
25
  with self._lock:
22
26
  if self._timer is not None:
@@ -2022,6 +2022,7 @@ def from_csv(
2022
2022
  def drain() -> None:
2023
2023
  try:
2024
2024
  headers: list[str] | None = list(columns) if columns else None
2025
+ rows_iter: Iterable[list[str]]
2025
2026
  if parse_line is not None:
2026
2027
  rows_iter = (parse_line(line) for line in source)
2027
2028
  else:
@@ -2475,7 +2476,7 @@ def from_nats(
2475
2476
  async for msg in sub:
2476
2477
  if not active[0]:
2477
2478
  return
2478
- actions.emit(_nats_msg_to_nats_message(msg, deserialize)) # type: ignore[arg-type]
2479
+ actions.emit(_nats_msg_to_nats_message(msg, deserialize))
2479
2480
  # Iterator exhausted — subscription closed (inline, matching TS pattern).
2480
2481
  if active[0]:
2481
2482
  actions.down([(MessageType.COMPLETE,)])
@@ -2501,7 +2502,7 @@ def from_nats(
2501
2502
  for msg in sub_or_coro:
2502
2503
  if not active[0]:
2503
2504
  return
2504
- actions.emit(_nats_msg_to_nats_message(msg, deserialize)) # type: ignore[arg-type]
2505
+ actions.emit(_nats_msg_to_nats_message(msg, deserialize))
2505
2506
  # Iterator exhausted — subscription closed.
2506
2507
  if active[0]:
2507
2508
  actions.down([(MessageType.COMPLETE,)])
@@ -2512,10 +2513,10 @@ def from_nats(
2512
2513
  t = threading.Thread(target=_run, daemon=True)
2513
2514
  t.start()
2514
2515
 
2515
- def cleanup() -> None:
2516
+ def _sync_cleanup() -> None:
2516
2517
  active[0] = False
2517
2518
 
2518
- return cleanup
2519
+ return _sync_cleanup
2519
2520
 
2520
2521
  return node(start, describe_kind="producer", complete_when_deps_complete=False)
2521
2522
 
@@ -2769,12 +2770,12 @@ def to_rabbitmq(
2769
2770
  if msg[0] is MessageType.DATA:
2770
2771
  value = msg[1] if len(msg) > 1 else None
2771
2772
  try:
2772
- rk = routing_key_extractor(value) # type: ignore[misc]
2773
+ rk = routing_key_extractor(value)
2773
2774
  except Exception as err:
2774
2775
  handler(SinkTransportError(stage="routing_key", error=err, value=value))
2775
2776
  return True
2776
2777
  try:
2777
- body = serialize(value) # type: ignore[misc]
2778
+ body = serialize(value)
2778
2779
  except Exception as err:
2779
2780
  handler(SinkTransportError(stage="serialize", error=err, value=value))
2780
2781
  return True
@@ -2908,7 +2909,7 @@ def to_file(
2908
2909
  if msg[0] is MessageType.DATA:
2909
2910
  value = msg[1] if len(msg) > 1 else None
2910
2911
  try:
2911
- line = serialize(value) # type: ignore[misc]
2912
+ line = serialize(value)
2912
2913
  except Exception as err:
2913
2914
  handler(SinkTransportError(stage="serialize", error=err, value=value))
2914
2915
  return True
@@ -3004,15 +3005,11 @@ def to_csv(
3004
3005
  header_written[0] = True
3005
3006
  header = delimiter.join(_escape_csv_field(c, delimiter) for c in columns)
3006
3007
  data = delimiter.join(
3007
- _escape_csv_field(cell_extractor(row, c), delimiter) # type: ignore[misc]
3008
- for c in columns
3008
+ _escape_csv_field(cell_extractor(row, c), delimiter) for c in columns
3009
3009
  )
3010
3010
  return header + "\n" + data + "\n"
3011
3011
  return (
3012
- delimiter.join(
3013
- _escape_csv_field(cell_extractor(row, c), delimiter) # type: ignore[misc]
3014
- for c in columns
3015
- )
3012
+ delimiter.join(_escape_csv_field(cell_extractor(row, c), delimiter) for c in columns)
3016
3013
  + "\n"
3017
3014
  )
3018
3015
 
@@ -3108,7 +3105,7 @@ def to_clickhouse(
3108
3105
  if msg[0] is MessageType.DATA:
3109
3106
  value = msg[1] if len(msg) > 1 else None
3110
3107
  try:
3111
- transformed = transform(value) # type: ignore[misc]
3108
+ transformed = transform(value)
3112
3109
  except Exception as err:
3113
3110
  handler(SinkTransportError(stage="serialize", error=err, value=value))
3114
3111
  return True
@@ -3224,7 +3221,7 @@ def to_s3(
3224
3221
  else:
3225
3222
  body = json.dumps(batch_data)
3226
3223
  content_type = "application/json"
3227
- key = key_generator(seq, wall_clock_ns()) # type: ignore[misc]
3224
+ key = key_generator(seq, wall_clock_ns())
3228
3225
  try:
3229
3226
  client.put_object(Bucket=bucket, Key=key, Body=body, ContentType=content_type)
3230
3227
  except Exception as err:
@@ -3254,7 +3251,7 @@ def to_s3(
3254
3251
  if msg[0] is MessageType.DATA:
3255
3252
  value = msg[1] if len(msg) > 1 else None
3256
3253
  try:
3257
- transformed = transform(value) # type: ignore[misc]
3254
+ transformed = transform(value)
3258
3255
  except Exception as err:
3259
3256
  handler(SinkTransportError(stage="serialize", error=err, value=value))
3260
3257
  return True
@@ -3330,7 +3327,7 @@ def to_postgres(
3330
3327
  if msg[0] is MessageType.DATA:
3331
3328
  value = msg[1] if len(msg) > 1 else None
3332
3329
  try:
3333
- sql, params = to_sql(value, table) # type: ignore[misc]
3330
+ sql, params = to_sql(value, table)
3334
3331
  except Exception as err:
3335
3332
  handler(SinkTransportError(stage="serialize", error=err, value=value))
3336
3333
  return True
@@ -3390,7 +3387,7 @@ def to_mongo(
3390
3387
  if msg[0] is MessageType.DATA:
3391
3388
  value = msg[1] if len(msg) > 1 else None
3392
3389
  try:
3393
- doc = to_document(value) # type: ignore[misc]
3390
+ doc = to_document(value)
3394
3391
  except Exception as err:
3395
3392
  handler(SinkTransportError(stage="serialize", error=err, value=value))
3396
3393
  return True
@@ -3456,7 +3453,7 @@ def to_loki(
3456
3453
  if msg[0] is MessageType.DATA:
3457
3454
  value = msg[1] if len(msg) > 1 else None
3458
3455
  try:
3459
- line = to_line(value) # type: ignore[misc]
3456
+ line = to_line(value)
3460
3457
  except Exception as err:
3461
3458
  handler(SinkTransportError(stage="serialize", error=err, value=value))
3462
3459
  return True
@@ -3523,7 +3520,7 @@ def to_tempo(
3523
3520
  if msg[0] is MessageType.DATA:
3524
3521
  value = msg[1] if len(msg) > 1 else None
3525
3522
  try:
3526
- spans = to_resource_spans(value) # type: ignore[misc]
3523
+ spans = to_resource_spans(value)
3527
3524
  except Exception as err:
3528
3525
  handler(SinkTransportError(stage="serialize", error=err, value=value))
3529
3526
  return True
@@ -3792,7 +3789,7 @@ def to_sqlite(
3792
3789
  if msg[0] is MessageType.DATA:
3793
3790
  value = msg[1] if len(msg) > 1 else None
3794
3791
  try:
3795
- sql, params = to_sql(value, table) # type: ignore[misc]
3792
+ sql, params = to_sql(value, table)
3796
3793
  except Exception as err:
3797
3794
  handler(SinkTransportError(stage="serialize", error=err, value=value))
3798
3795
  return True
@@ -5,6 +5,9 @@ from __future__ import annotations
5
5
  from collections import OrderedDict
6
6
  from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar, runtime_checkable
7
7
 
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Sequence
10
+
8
11
  from graphrefly.core.protocol import MessageType
9
12
  from graphrefly.core.sugar import state
10
13
 
@@ -155,7 +158,7 @@ class CascadingCache(Generic[V]): # noqa: UP046
155
158
 
156
159
  def __init__(
157
160
  self,
158
- tiers: list[CacheTier],
161
+ tiers: Sequence[CacheTier],
159
162
  *,
160
163
  max_size: int = 0,
161
164
  eviction: EvictionPolicy | None = None,
@@ -171,7 +174,7 @@ class CascadingCache(Generic[V]): # noqa: UP046
171
174
  for i in range(hit_tier):
172
175
  tier = self._tiers[i]
173
176
  if _tier_has_save(tier):
174
- tier.save(key, value) # type: ignore[union-attr]
177
+ tier.save(key, value)
175
178
 
176
179
  def _cascade(self, key: str, nd: Node[Any]) -> None:
177
180
  for tier_index, tier in enumerate(self._tiers):
@@ -199,10 +202,10 @@ class CascadingCache(Generic[V]): # noqa: UP046
199
202
  # Demote to deepest tier with save before evicting
200
203
  for i in range(len(self._tiers) - 1, -1, -1):
201
204
  if _tier_has_save(self._tiers[i]):
202
- self._tiers[i].save(victim, value) # type: ignore[union-attr]
205
+ self._tiers[i].save(victim, value)
203
206
  for j in range(i):
204
207
  if _tier_has_clear(self._tiers[j]):
205
- self._tiers[j].clear(victim) # type: ignore[union-attr]
208
+ self._tiers[j].clear(victim)
206
209
  break
207
210
  nd.down([(MessageType.TEARDOWN,)])
208
211
  del self._entries[victim]
@@ -236,10 +239,9 @@ class CascadingCache(Generic[V]): # noqa: UP046
236
239
  if self._write_through:
237
240
  for tier in self._tiers:
238
241
  if _tier_has_save(tier):
239
- tier.save(key, value) # type: ignore[union-attr]
242
+ tier.save(key, value)
240
243
  elif self._tiers and _tier_has_save(self._tiers[0]):
241
- self._tiers[0].save(key, value) # type: ignore[union-attr]
242
-
244
+ self._tiers[0].save(key, value)
243
245
  if key in self._entries:
244
246
  self._entries[key].down([(MessageType.DATA, value)])
245
247
  if self._eviction is not None:
@@ -271,7 +273,7 @@ class CascadingCache(Generic[V]): # noqa: UP046
271
273
  self._eviction.delete(key)
272
274
  for tier in self._tiers:
273
275
  if _tier_has_clear(tier):
274
- tier.clear(key) # type: ignore[union-attr]
276
+ tier.clear(key)
275
277
 
276
278
  def has(self, key: str) -> bool:
277
279
  """Check if a key is in the in-memory entries."""
@@ -284,7 +286,7 @@ class CascadingCache(Generic[V]): # noqa: UP046
284
286
 
285
287
 
286
288
  def cascading_cache(
287
- tiers: list[CacheTier],
289
+ tiers: Sequence[CacheTier],
288
290
  *,
289
291
  max_size: int = 0,
290
292
  eviction: EvictionPolicy | None = None,
@@ -398,5 +400,10 @@ def tiered_storage(
398
400
  ```
399
401
  """
400
402
  tiers = [_CheckpointTier(a) for a in adapters]
401
- inner = CascadingCache(tiers, max_size=max_size, eviction=eviction, write_through=True)
403
+ inner: CascadingCache[Any] = CascadingCache(
404
+ tiers,
405
+ max_size=max_size,
406
+ eviction=eviction,
407
+ write_through=True,
408
+ )
402
409
  return TieredStorage(inner)
@@ -796,7 +796,7 @@ def fallback(source: Node[Any], fb: Any) -> Node[Any]:
796
796
  actions.down([m])
797
797
 
798
798
  unsub_holder[0] = source.subscribe(sink)
799
- unsub = unsub_holder[0]
799
+ unsub: Callable[[], None] = unsub_holder[0] # type: ignore[assignment]
800
800
 
801
801
  def cleanup() -> None:
802
802
  unsub()
@@ -853,7 +853,8 @@ def timeout(source: Node[Any], timeout_ns: int) -> Node[Any]:
853
853
  done[0] = True
854
854
  # §5.10: ResettableTimer (not from_timer) — resettable
855
855
  # deadline; from_timer adds Node overhead per DATA reset.
856
- unsub_holder[0]()
856
+ if unsub_holder[0] is not None:
857
+ unsub_holder[0]()
857
858
  actions.down([(MessageType.ERROR, TimeoutError(timeout_ns))])
858
859
 
859
860
  to_timer.start(delay_s, fire)
@@ -890,7 +891,7 @@ def timeout(source: Node[Any], timeout_ns: int) -> Node[Any]:
890
891
 
891
892
  arm_timer()
892
893
  unsub_holder[0] = source.subscribe(sink)
893
- unsub = unsub_holder[0]
894
+ unsub: Callable[[], None] = unsub_holder[0] # type: ignore[assignment]
894
895
 
895
896
  def cleanup() -> None:
896
897
  done[0] = True
@@ -10,11 +10,11 @@ import threading
10
10
  from collections import deque
11
11
  from contextlib import contextmanager, suppress
12
12
  from dataclasses import dataclass, field
13
- from typing import TYPE_CHECKING, Any, ClassVar
13
+ from typing import TYPE_CHECKING, Any, ClassVar, cast
14
14
 
15
15
  from graphrefly.core.clock import monotonic_ns
16
16
  from graphrefly.core.guard import GuardDenied, normalize_actor
17
- from graphrefly.core.meta import describe_node, resolve_describe_fields
17
+ from graphrefly.core.meta import DescribeDetail, describe_node, resolve_describe_fields
18
18
  from graphrefly.core.node import NodeImpl
19
19
  from graphrefly.core.protocol import Messages, MessageType, is_batching, message_tier
20
20
  from graphrefly.core.sugar import state
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
23
23
  from collections.abc import Callable, Iterator
24
24
 
25
25
 
26
- class DescribeResult(dict):
26
+ class DescribeResult(dict[str, Any]):
27
27
  """Dict subclass returned by :meth:`Graph.describe`.
28
28
 
29
29
  Provides an ``expand()`` method for re-reading the live graph at a higher
@@ -34,7 +34,7 @@ class DescribeResult(dict):
34
34
  def expand(self, detail_or_fields: Any = None) -> DescribeResult:
35
35
  """Re-read the live graph at a higher detail level or with explicit fields."""
36
36
  fn = object.__getattribute__(self, "_expand_fn")
37
- return fn(detail_or_fields)
37
+ return cast("DescribeResult", fn(detail_or_fields))
38
38
 
39
39
 
40
40
  @dataclass(frozen=True, slots=True)
@@ -1050,7 +1050,7 @@ class Graph:
1050
1050
  detail: str | None = None,
1051
1051
  fields: list[str] | None = None,
1052
1052
  format: str | None = None,
1053
- ) -> dict[str, Any]:
1053
+ ) -> DescribeResult:
1054
1054
  """Static structure snapshot (GRAPHREFLY-SPEC §3.6, Appendix B).
1055
1055
 
1056
1056
  ``nodes`` keys are qualified paths (including ``::__meta__::`` for companions).
@@ -1099,7 +1099,7 @@ class Graph:
1099
1099
  if format == "spec":
1100
1100
  include_fields: set[str] | None = {"type", "deps"}
1101
1101
  else:
1102
- include_fields = resolve_describe_fields(detail, fields)
1102
+ include_fields = resolve_describe_fields(cast("DescribeDetail | None", detail), fields)
1103
1103
 
1104
1104
  targets = _collect_observe_targets(self, "")
1105
1105
  paths_by_id = {id(n): p for p, n in targets}
@@ -2057,7 +2057,14 @@ class Graph:
2057
2057
  va = na.get(key)
2058
2058
  vb = nb.get(key)
2059
2059
  if va != vb:
2060
- changed_nodes.append({"path": p, "field": key, "from": va, "to": vb})
2060
+ changed_nodes.append(
2061
+ GraphDiffNodeChange(
2062
+ path=p,
2063
+ field=key,
2064
+ from_value=va,
2065
+ to_value=vb,
2066
+ )
2067
+ )
2061
2068
 
2062
2069
  a_edges = {(e["from"], e["to"]) for e in a.get("edges", [])}
2063
2070
  b_edges = {(e["from"], e["to"]) for e in b.get("edges", [])}