graphrefly 0.5.0__tar.gz → 0.6.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.
- {graphrefly-0.5.0 → graphrefly-0.6.0}/CHANGELOG.md +13 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/PKG-INFO +32 -16
- {graphrefly-0.5.0 → graphrefly-0.6.0}/README.md +30 -14
- {graphrefly-0.5.0 → graphrefly-0.6.0}/docs/optimizations.md +14 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/docs/roadmap.md +3 -3
- {graphrefly-0.5.0/website/public → graphrefly-0.6.0}/llms.txt +3 -1
- {graphrefly-0.5.0 → graphrefly-0.6.0}/pyproject.toml +10 -8
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/guard.py +12 -2
- graphrefly-0.6.0/src/graphrefly/core/meta.py +246 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/__init__.py +36 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/adapters.py +613 -104
- graphrefly-0.6.0/src/graphrefly/extra/cascading_cache.py +402 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/checkpoint.py +82 -63
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/composite.py +22 -12
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/resilience.py +270 -2
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/graph/__init__.py +2 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/graph/graph.py +240 -58
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/integrations/fastapi.py +26 -17
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/ai.py +41 -25
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/cqrs.py +31 -16
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/orchestration.py +9 -6
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_adapters_ingest.py +67 -79
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_adapters_storage.py +498 -21
- graphrefly-0.6.0/tests/test_cascading_cache.py +261 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_extra_composite.py +19 -19
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_extra_resilience.py +148 -6
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_graph.py +251 -11
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_guard.py +3 -3
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_patterns_ai.py +12 -11
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_patterns_cqrs.py +7 -5
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_patterns_orchestration.py +2 -2
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_reactive_layout.py +1 -1
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_regressions.py +1 -1
- graphrefly-0.5.0/src/graphrefly/core/meta.py +0 -149
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.claude/skills/dev-dispatch/SKILL.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.claude/skills/parity/SKILL.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.claude/skills/qa/SKILL.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.gemini/skills/dev-dispatch/SKILL.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.gemini/skills/parity/SKILL.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.github/workflows/pages.yml +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.github/workflows/release.yml +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.gitignore +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/.mise.toml +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/CLAUDE.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/CONTRIBUTING.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/GEMINI.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/LICENSE +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/archive/docs/DESIGN-ARCHIVE-INDEX.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/archive/docs/SESSION-access-control-actor-guard.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/archive/docs/SESSION-cross-repo-implementation-audit.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/archive/docs/SESSION-demo-test-strategy.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/archive/docs/SESSION-graphrefly-spec-design.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/archive/docs/SESSION-serialization-memory-footprint.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/archive/docs/SESSION-tier2-parity-nonlocal-forward-inner.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/archive/docs/SESSION-universal-reduction-layer.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/benchmarks/py-baseline.json +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/docs/ADAPTER-CONTRACT.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/docs/benchmark.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/docs/docs-guidance.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/docs/test-guidance.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/examples/README.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/examples/basic_counter.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/__init__.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/compat/__init__.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/compat/async_utils.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/compat/asyncio_runner.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/compat/trio_runner.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/__init__.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/cancellation.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/clock.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/dynamic_node.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/node.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/protocol.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/runner.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/subgraph_locks.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/sugar.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/core/versioning.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/backoff.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/backpressure.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/cron.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/data_structures.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/sources.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/tier1.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/extra/tier2.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/integrations/__init__.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/__init__.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/memory.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/messaging.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/reactive_layout/__init__.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/reactive_layout/measurement_adapters.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/reactive_layout/reactive_block_layout.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/patterns/reactive_layout/reactive_layout.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/src/graphrefly/py.typed +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/bench_core.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/conftest.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_adapter_contract.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_backpressure.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_concurrency.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_core.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_dynamic_node.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_edge_cases.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_extra_data_structures.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_extra_sources.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_extra_sources_http.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_extra_tier1.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_extra_tier2.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_fastapi.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_measurement_adapters.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_patterns_memory.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_patterns_messaging.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_perf_smoke.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_protocol.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_reactive_block_layout.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_runner.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_smoke.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_sugar.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/tests/test_versioning.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/.gitignore +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/README.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/astro.config.mjs +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/content.config.ts +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/package.json +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/pnpm-lock.yaml +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0/website/public}/llms.txt +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/py-api-sidebar.mjs +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/scripts/gen_api_docs.py +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/scripts/sync-docs.mjs +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/components/GraphreflyHero.astro +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/components/Header.astro +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/components/MobileMenuFooter.astro +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/components/PyodidePlayground.tsx +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/components/Sidebar.astro +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/components/SiteTitle.astro +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/BackoffPreset.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/BackoffStrategy.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/CheckpointAdapter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/CircuitBreaker.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/CircuitOpenError.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/DeferWhen.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/DictCheckpointAdapter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/DistillBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/EmitStrategy.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/Extraction.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/FileCheckpointAdapter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/HttpBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/JitterMode.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/MemoryCheckpointAdapter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/Message.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/MessageType.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/Messages.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/NodeActions.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/NodeFn.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/NodeImpl.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/NodeStatus.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/PipeOperator.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/PubSubHub.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/ReactiveIndexBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/ReactiveListBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/ReactiveLogBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/ReactiveMapBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/SqliteCheckpointAdapter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/SubscribeHints.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/TokenBucket.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/VerifiableBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/Versioned.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/WithBreakerBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/WithStatusBundle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/audit.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/batch.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/buffer.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/buffer_count.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/buffer_time.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/cached.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/checkpoint_node_value.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/circuit_breaker.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/combine.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/concat.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/concat_map.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/constant.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/debounce.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/decorrelated_jitter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/delay.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/derived.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/dispatch_messages.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/distill.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/distinct_until_changed.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/effect.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/element_at.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/emit_with_batch.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/empty.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/exhaust_map.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/exponential.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/fibonacci.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/filter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/find.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/first.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/first_value_from.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/flat_map.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/for_each.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_any.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_async_iter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_awaitable.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_cron.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_event_emitter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_fs_watch.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_git_hook.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_http.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_iter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_mcp.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_timer.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_webhook.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/from_websocket.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/gate.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/index.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/interval.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/is_batching.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/is_phase2_message.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/is_terminal_message.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/last.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/linear.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/log_slice.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/map.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/merge.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/message_tier.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/never.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/node.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/of.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/operator.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/pairwise.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/partition_for_batch.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/pausable.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/pipe.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/producer.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/propagates_to_meta.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/pubsub.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/race.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/rate_limiter.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/reactive_index.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/reactive_list.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/reactive_log.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/reactive_map.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/reduce.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/repeat.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/replay.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/rescue.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/resolve_backoff_preset.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/restore_graph_checkpoint.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/retry.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/sample.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/save_graph_checkpoint.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/scan.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/share.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/skip.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/start_with.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/state.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/subscribe.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/switch_map.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/take.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/take_until.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/take_while.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/tap.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/throttle.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/throw_error.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/timeout.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/to_array.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/to_list.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/to_sse.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/to_websocket.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/token_bucket.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/token_tracker.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/verifiable.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/window.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/window_count.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/window_time.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/with_breaker.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/with_latest_from.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/with_max_attempts.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/with_status.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/api/zip.md +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/index.mdx +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content/docs/lab/python.mdx +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/content.config.ts +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/env.d.ts +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/src/styles/custom.css +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/theme-prototypes.html +0 -0
- {graphrefly-0.5.0 → graphrefly-0.6.0}/website/tsconfig.json +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v0.6.0 (2026-04-05)
|
|
6
|
+
|
|
7
|
+
### Chores
|
|
8
|
+
|
|
9
|
+
- Inspector + data_class
|
|
10
|
+
([`5bfb409`](https://github.com/graphrefly/graphrefly-py/commit/5bfb409eb62a82cc72d7b4d404981789ded3a3ca))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- 3.1c
|
|
15
|
+
([`914e424`](https://github.com/graphrefly/graphrefly-py/commit/914e424e7f934b71e407da187536c16cb63ee43a))
|
|
16
|
+
|
|
17
|
+
|
|
5
18
|
## v0.5.0 (2026-04-05)
|
|
6
19
|
|
|
7
20
|
### Chores
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphrefly
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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,
|
|
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
|
-
**
|
|
28
|
+
**Describe what matters. It watches, filters, and explains — persistently.**
|
|
29
29
|
|
|
30
|
-
|
|
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
|
[](https://pypi.org/project/graphrefly/)
|
|
33
33
|
[](./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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
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
|
-
**
|
|
3
|
+
**Describe what matters. It watches, filters, and explains — persistently.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
[](https://pypi.org/project/graphrefly/)
|
|
8
8
|
[](./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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
|
39
|
-
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
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,6 +124,16 @@ 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):** `_emit_auto_value` must skip `equals()` when `_cached` is still `None` (initial state before first DATA). TS fixed in `node.ts`; PY needs matching fix in `node.py`. Spec §2.5 updated: `equals` only compares two real DATA values, never `undefined`/`None`. **Audit all custom `equals` in PY** to verify none depend on seeing `None`. See also TS `docs/optimizations.md` and roadmap hotfix section.
|
|
130
|
+
- **`_cached is None` sentinel is ambiguous with `[(DATA, None)]` and INVALIDATE (noted 2026-04-05):** The current guard uses `self._cached is not None` to detect "never emitted." This conflates: (a) never emitted, (b) emitted `[(DATA, None)]`, (c) reset via INVALIDATE/`reset_on_teardown`. **Proposed fix:** Add `_has_emitted_data: bool` flag (initially `False`, set `True` on first `_emit_auto_value`, reset on INVALIDATE/`reset_on_teardown`). Both TS and PY. Counter-argument: `_run_fn` skips `None` returns so auto-emit can never produce `None`; the `actions.emit(None)` path is exotic. Deferred: decide and implement in both languages.
|
|
131
|
+
- **Derived node error observability gap (all phases, noted 2026-04-05, partially resolved 2026-04-05):** When a derived node's `fn` or `equals` throws, the node emits `[(ERROR, err)]` and status becomes `"errored"`. But `graph.get()` returns `None` — indistinguishable from "never computed." Resolution: spec §2.2 updated to emphasize `status` as source of truth and `describe()` as the primary diagnostic — `get()` is a value accessor only. Still needed: wrap `equals`/`fn` errors with node name context before emitting ERROR — deferred to next chat.
|
|
132
|
+
- **PY `retry` lock scope around `connect()` (Phase 3.1, noted 2026-04-05):** In `resilience.py`, `schedule_retry_or_finish` calls `fire()` which calls `connect()` while holding the internal lock. `connect()` runs `source.subscribe(sink)` outside the lock, creating a window where concurrent `cleanup()` could miss the new subscription — a potential resource leak on rapid teardown + retry. Mitigation deferred: the window is narrow and only affects concurrent unsubscribe during active retry.
|
|
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. Not an issue in single-threaded usage (the common case). Deferred: add `check_same_thread=False` + a `threading.Lock` if multi-threaded checkpoint usage becomes a pattern.
|
|
134
|
+
- **Sink `value = msg[1] if len(msg) > 1 else None` silent None coercion (Phase 5.2–5.3, noted 2026-04-05):** All per-record sinks extract DATA payload via `msg[1] if len(msg) > 1 else None`, silently treating a malformed DATA (missing payload) as `None`. This masks upstream bugs. Deferred: pre-existing pattern across all sinks; fixing would require a coordinated change to all adapters + a spec decision on whether payload-less DATA is valid.
|
|
135
|
+
- **Sink `suppress(Exception)` in `dispose()` hides teardown bugs (Phase 5.2–5.3, noted 2026-04-05):** All sinks use `with suppress(Exception)` around `errors_node.down([(MessageType.TEARDOWN,)])` in `dispose()`. This silently swallows teardown errors (double-dispose, graph corruption). Deferred: pre-existing pattern across all sinks; narrowing the suppression requires auditing what errors are actually possible during teardown.
|
|
136
|
+
- **`from_django_orm` and `from_tortoise` have identical implementations (Phase 5.2, noted 2026-04-05):** Both sources iterate a sync iterable and emit DATA per row. Bodies are character-identical. Intentional: separate entry points with distinct docstrings and `from_tortoise` includes an async-guard that rejects unawaited coroutines/async iterables. A shared `_from_sync_iterable` helper could reduce duplication but would obscure the distinct adapter semantics. Deferred: revisit if more iterable-based sources are added.
|
|
127
137
|
|
|
128
138
|
---
|
|
129
139
|
|
|
@@ -131,6 +141,10 @@ Union-find over node identity merges components when nodes list dependencies at
|
|
|
131
141
|
|
|
132
142
|
**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
143
|
|
|
144
|
+
- **Dual-bookkeeping of derived deps + connect edges (all phases, noted 2026-04-05):** In both TS and PY, `derived([depA, depB], fn)` declares reactive deps at the node level (DIRTY/DATA propagation), but `Graph` only records named edges via explicit `graph.connect(from, to)`. Both declarations are required: the node deps drive the reactive engine, the connect edges drive `describe()` / `to_mermaid()` / `to_d2()` output. If a dep is added/removed in the derived call but the corresponding `connect` is not updated (or vice versa), the introspection output diverges from actual dataflow. All graph factories (orchestration, cqrs, memory, messaging) follow this dual pattern. A future improvement could auto-register edges from constructor deps when a node is `add()`-ed — but this requires the Graph to inspect the node's internal dep list, which currently crosses the node/graph encapsulation boundary. Deferred: revisit if the divergence-risk causes real bugs.
|
|
145
|
+
|
|
146
|
+
- **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` all use `setTimeout`/`threading.Timer` for resettable deadlines — documented with inline `§5.10` comments; a reusable resettable timer primitive is deferred.
|
|
147
|
+
- **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
148
|
- **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
149
|
- **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
150
|
|
|
@@ -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
|
-
- [
|
|
303
|
-
- [
|
|
304
|
-
- [
|
|
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
|
-
|
|
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.
|
|
3
|
+
version = "0.6.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
|
-
"
|
|
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
|
-
"
|
|
24
|
-
"reactive-programming",
|
|
26
|
+
"zero-dependency",
|
|
25
27
|
]
|
|
26
28
|
classifiers = [
|
|
27
29
|
"Development Status :: 3 - Alpha",
|
|
@@ -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
|
-
|
|
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
|
|
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",
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Meta companion stores — graphrefly-ts ``src/core/meta.ts``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
|
|
12
|
+
from graphrefly.core.dynamic_node import DynamicNodeImpl
|
|
13
|
+
from graphrefly.core.guard import access_hint_for_guard
|
|
14
|
+
from graphrefly.core.node import NodeImpl # noqa: TC001 — runtime type for describe_node
|
|
15
|
+
from graphrefly.core.versioning import is_v1
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"DescribeDetail",
|
|
19
|
+
"DescribeField",
|
|
20
|
+
"describe_node",
|
|
21
|
+
"meta_snapshot",
|
|
22
|
+
"resolve_describe_fields",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Progressive disclosure types (Phase 3.3b)
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
DescribeDetail = Literal["minimal", "standard", "full"]
|
|
30
|
+
DescribeField = str # "type", "status", "value", "deps", "meta", "v", etc.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_describe_fields(
|
|
34
|
+
detail: DescribeDetail | None = None,
|
|
35
|
+
fields: list[str] | None = None,
|
|
36
|
+
) -> set[str] | None:
|
|
37
|
+
"""Resolve which fields to include. fields overrides detail. None = all fields."""
|
|
38
|
+
if fields is not None and len(fields) > 0:
|
|
39
|
+
return set(fields)
|
|
40
|
+
if detail == "standard":
|
|
41
|
+
return {"type", "status", "value", "deps", "meta", "v"}
|
|
42
|
+
if detail == "full":
|
|
43
|
+
return None # all
|
|
44
|
+
# minimal (default)
|
|
45
|
+
return {"type", "deps"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _MetaNode(Protocol):
|
|
49
|
+
"""Parent node: read-only ``meta`` mapping (e.g. ``MappingProxyType``)."""
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def meta(self) -> Mapping[str, Any]: ...
|
|
53
|
+
|
|
54
|
+
def get(self) -> Any: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def meta_snapshot(node: _MetaNode) -> dict[str, Any]:
|
|
58
|
+
"""Merge current meta field values into a plain dict for describe-style JSON.
|
|
59
|
+
|
|
60
|
+
Values come from each companion node's ``get()`` (last settled cache). If a meta
|
|
61
|
+
field is dirty (``DIRTY`` received, ``DATA`` pending), the snapshot holds the
|
|
62
|
+
previous value — check ``node.meta[key].status`` when freshness matters.
|
|
63
|
+
Keys whose companion ``get()`` raises are omitted so describe tooling keeps other
|
|
64
|
+
fields intact.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
node: Any object with a ``meta`` mapping of ``str -> Node`` and a ``get()`` method.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A plain ``dict`` mapping each meta key to its last settled value.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
```python
|
|
74
|
+
from graphrefly import state
|
|
75
|
+
from graphrefly.core.meta import meta_snapshot
|
|
76
|
+
x = state(0, meta={"version": 1})
|
|
77
|
+
snap = meta_snapshot(x)
|
|
78
|
+
# snap == {"version": 1}
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
out: dict[str, Any] = {}
|
|
82
|
+
for key, child in node.meta.items():
|
|
83
|
+
with suppress(Exception):
|
|
84
|
+
out[key] = child.get()
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _infer_describe_type(n: NodeImpl[Any]) -> str:
|
|
89
|
+
"""Best-effort ``type`` for GRAPHREFLY-SPEC describe node shape (§3.6, Appendix B)."""
|
|
90
|
+
if n._describe_kind is not None:
|
|
91
|
+
return n._describe_kind
|
|
92
|
+
if not n._has_deps:
|
|
93
|
+
return "state" if n._fn is None else "producer"
|
|
94
|
+
if n._fn is None:
|
|
95
|
+
return "derived"
|
|
96
|
+
if n._manual_emit_used:
|
|
97
|
+
return "operator"
|
|
98
|
+
return "derived"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _wants_field(include_fields: set[str] | None, field: str) -> bool:
|
|
102
|
+
"""Return True if *field* should be included given *include_fields*."""
|
|
103
|
+
if include_fields is None:
|
|
104
|
+
return True
|
|
105
|
+
return field in include_fields
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _wants_opt_in_field(include_fields: set[str] | None, *names: str) -> bool:
|
|
109
|
+
"""Return True when one of *names* is included in the resolved field set.
|
|
110
|
+
|
|
111
|
+
``None`` means full mode (all fields) so opt-in fields are included.
|
|
112
|
+
For preset detail levels (``"minimal"``, ``"standard"``), the set won't
|
|
113
|
+
contain these names, so they stay excluded unless explicitly requested via
|
|
114
|
+
``fields``.
|
|
115
|
+
"""
|
|
116
|
+
if include_fields is None:
|
|
117
|
+
return True # full mode → include everything
|
|
118
|
+
return any(n in include_fields for n in names)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _wants_meta(include_fields: set[str] | None) -> bool:
|
|
122
|
+
"""Return True if any meta content should be included."""
|
|
123
|
+
if include_fields is None:
|
|
124
|
+
return True
|
|
125
|
+
if "meta" in include_fields:
|
|
126
|
+
return True
|
|
127
|
+
return any(f.startswith("meta.") for f in include_fields)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _filter_meta(full_meta: dict[str, Any], include_fields: set[str]) -> dict[str, Any]:
|
|
131
|
+
"""When dotted meta paths are requested (e.g. 'meta.label'), return only those keys."""
|
|
132
|
+
if "meta" in include_fields:
|
|
133
|
+
return full_meta # whole meta requested
|
|
134
|
+
dotted = {f.split(".", 1)[1] for f in include_fields if f.startswith("meta.")}
|
|
135
|
+
if not dotted:
|
|
136
|
+
return full_meta
|
|
137
|
+
return {k: v for k, v in full_meta.items() if k in dotted}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def describe_node(
|
|
141
|
+
n: NodeImpl[Any] | DynamicNodeImpl[Any],
|
|
142
|
+
include_fields: set[str] | None = None,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""Build a single-node slice of ``Graph.describe()`` JSON (structure + ``meta`` snapshot).
|
|
145
|
+
|
|
146
|
+
The ``meta`` field uses :func:`meta_snapshot` (plain values), matching the
|
|
147
|
+
spec's describe shape (GRAPHREFLY-SPEC Appendix B). ``type`` is inferred from
|
|
148
|
+
factory configuration, optional ``describe_kind`` in node options, and the last
|
|
149
|
+
``_manual_emit_used`` hint (operator vs derived). Sugar constructors
|
|
150
|
+
(``effect``, ``producer``, ``derived``) set ``describe_kind`` automatically.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
n: A :class:`~graphrefly.core.node.NodeImpl` or
|
|
154
|
+
:class:`~graphrefly.core.dynamic_node.DynamicNodeImpl` to describe.
|
|
155
|
+
include_fields: Optional set of field names to include. ``None`` means all
|
|
156
|
+
fields (full mode). ``type`` and ``deps`` are always included.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A ``dict`` with keys ``type``, ``deps``, and optionally ``status``, ``meta``,
|
|
160
|
+
``name``, ``value``, ``v``, ``guard``, and ``last_mutation``.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
```python
|
|
164
|
+
from graphrefly import state
|
|
165
|
+
from graphrefly.core.meta import describe_node
|
|
166
|
+
x = state(42, name="x")
|
|
167
|
+
d = describe_node(x)
|
|
168
|
+
assert d["type"] == "state"
|
|
169
|
+
assert d["value"] == 42
|
|
170
|
+
```
|
|
171
|
+
"""
|
|
172
|
+
if isinstance(n, DynamicNodeImpl):
|
|
173
|
+
out: dict[str, Any] = {
|
|
174
|
+
"type": n._describe_kind or "derived",
|
|
175
|
+
"deps": [],
|
|
176
|
+
}
|
|
177
|
+
if _wants_field(include_fields, "status"):
|
|
178
|
+
out["status"] = n.status
|
|
179
|
+
if _wants_meta(include_fields):
|
|
180
|
+
full_meta = meta_snapshot(n)
|
|
181
|
+
g = n._guard
|
|
182
|
+
if g is not None and "access" not in full_meta:
|
|
183
|
+
full_meta["access"] = access_hint_for_guard(g)
|
|
184
|
+
if include_fields is not None:
|
|
185
|
+
out["meta"] = _filter_meta(full_meta, include_fields)
|
|
186
|
+
else:
|
|
187
|
+
out["meta"] = full_meta
|
|
188
|
+
if n.name is not None:
|
|
189
|
+
out["name"] = n.name
|
|
190
|
+
if _wants_field(include_fields, "value"):
|
|
191
|
+
with suppress(Exception):
|
|
192
|
+
out["value"] = n.get()
|
|
193
|
+
# Guard access hint (standalone opt-in field)
|
|
194
|
+
if _wants_opt_in_field(include_fields, "guard") and n._guard is not None:
|
|
195
|
+
out["guard"] = access_hint_for_guard(n._guard)
|
|
196
|
+
# last_mutation (opt-in)
|
|
197
|
+
if _wants_opt_in_field(include_fields, "lastMutation", "last_mutation"):
|
|
198
|
+
lm = n.last_mutation
|
|
199
|
+
if lm is not None:
|
|
200
|
+
out["last_mutation"] = asdict(lm)
|
|
201
|
+
# Versioning (GRAPHREFLY-SPEC §7)
|
|
202
|
+
if _wants_field(include_fields, "v") and hasattr(n, "v") and n.v is not None:
|
|
203
|
+
out["v"] = _versioning_dict(n.v)
|
|
204
|
+
return out
|
|
205
|
+
|
|
206
|
+
out = {
|
|
207
|
+
"type": _infer_describe_type(n),
|
|
208
|
+
"deps": [d.name or "" for d in n._deps],
|
|
209
|
+
}
|
|
210
|
+
if _wants_field(include_fields, "status"):
|
|
211
|
+
out["status"] = n.status
|
|
212
|
+
if _wants_meta(include_fields):
|
|
213
|
+
full_meta = meta_snapshot(n)
|
|
214
|
+
g = n._guard
|
|
215
|
+
if g is not None and "access" not in full_meta:
|
|
216
|
+
full_meta["access"] = access_hint_for_guard(g)
|
|
217
|
+
if include_fields is not None:
|
|
218
|
+
out["meta"] = _filter_meta(full_meta, include_fields)
|
|
219
|
+
else:
|
|
220
|
+
out["meta"] = full_meta
|
|
221
|
+
if n.name is not None:
|
|
222
|
+
out["name"] = n.name
|
|
223
|
+
if _wants_field(include_fields, "value"):
|
|
224
|
+
with suppress(Exception):
|
|
225
|
+
out["value"] = n.get()
|
|
226
|
+
# Guard access hint (standalone opt-in field)
|
|
227
|
+
if _wants_opt_in_field(include_fields, "guard") and n._guard is not None:
|
|
228
|
+
out["guard"] = access_hint_for_guard(n._guard)
|
|
229
|
+
# last_mutation (opt-in)
|
|
230
|
+
if _wants_opt_in_field(include_fields, "lastMutation", "last_mutation"):
|
|
231
|
+
lm = n.last_mutation
|
|
232
|
+
if lm is not None:
|
|
233
|
+
out["last_mutation"] = asdict(lm)
|
|
234
|
+
# Versioning (GRAPHREFLY-SPEC §7)
|
|
235
|
+
if _wants_field(include_fields, "v") and n.v is not None:
|
|
236
|
+
out["v"] = _versioning_dict(n.v)
|
|
237
|
+
return out
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _versioning_dict(v: Any) -> dict[str, Any]:
|
|
241
|
+
"""Convert versioning info to a plain dict for JSON serialization."""
|
|
242
|
+
d: dict[str, Any] = {"id": v.id, "version": v.version}
|
|
243
|
+
if is_v1(v):
|
|
244
|
+
d["cid"] = v.cid
|
|
245
|
+
d["prev"] = v.prev
|
|
246
|
+
return d
|