activegraph 1.0.3__tar.gz → 1.0.4__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 (158) hide show
  1. {activegraph-1.0.3 → activegraph-1.0.4}/PKG-INFO +1 -1
  2. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/__init__.py +1 -1
  3. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/core/graph.py +27 -0
  4. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph.egg-info/PKG-INFO +1 -1
  5. {activegraph-1.0.3 → activegraph-1.0.4}/pyproject.toml +1 -1
  6. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_graph.py +98 -0
  7. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_requeue_unfired.py +64 -0
  8. {activegraph-1.0.3 → activegraph-1.0.4}/README.md +0 -0
  9. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/__main__.py +0 -0
  10. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/behaviors/__init__.py +0 -0
  11. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/behaviors/base.py +0 -0
  12. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/behaviors/decorators.py +0 -0
  13. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/cli/__init__.py +0 -0
  14. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/cli/main.py +0 -0
  15. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/cli/quickstart.py +0 -0
  16. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/core/__init__.py +0 -0
  17. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/core/clock.py +0 -0
  18. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/core/event.py +0 -0
  19. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/core/ids.py +0 -0
  20. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/core/patch.py +0 -0
  21. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/core/view.py +0 -0
  22. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/errors.py +0 -0
  23. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/frame.py +0 -0
  24. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/__init__.py +0 -0
  25. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/anthropic.py +0 -0
  26. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/cache.py +0 -0
  27. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/errors.py +0 -0
  28. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/openai.py +0 -0
  29. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/parsing.py +0 -0
  30. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/prompt.py +0 -0
  31. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/provider.py +0 -0
  32. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/recorded.py +0 -0
  33. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/llm/types.py +0 -0
  34. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/observability/__init__.py +0 -0
  35. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/observability/logging.py +0 -0
  36. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/observability/metrics.py +0 -0
  37. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/observability/migration.py +0 -0
  38. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/observability/prometheus.py +0 -0
  39. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/observability/status.py +0 -0
  40. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/__init__.py +0 -0
  41. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/__init__.py +0 -0
  42. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/behaviors.py +0 -0
  43. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  44. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  45. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/object_types.py +0 -0
  46. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  47. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  48. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  49. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  50. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/settings.py +0 -0
  51. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/diligence/tools.py +0 -0
  52. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/loader.py +0 -0
  53. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/packs/scaffold.py +0 -0
  54. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/policy.py +0 -0
  55. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/__init__.py +0 -0
  56. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/_live.py +0 -0
  57. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/behavior_graph.py +0 -0
  58. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/budget.py +0 -0
  59. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/config_errors.py +0 -0
  60. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/diff.py +0 -0
  61. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/errors.py +0 -0
  62. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/exec_errors.py +0 -0
  63. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/patterns.py +0 -0
  64. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/queue.py +0 -0
  65. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/registration_errors.py +0 -0
  66. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/registry.py +0 -0
  67. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/runtime.py +0 -0
  68. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/scheduler.py +0 -0
  69. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/runtime/view_builder.py +0 -0
  70. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/__init__.py +0 -0
  71. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/base.py +0 -0
  72. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/conformance.py +0 -0
  73. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/errors.py +0 -0
  74. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/memory.py +0 -0
  75. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/postgres.py +0 -0
  76. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/serde.py +0 -0
  77. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/sqlite.py +0 -0
  78. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/store/url.py +0 -0
  79. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/__init__.py +0 -0
  80. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/base.py +0 -0
  81. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/cache.py +0 -0
  82. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/context.py +0 -0
  83. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/decorators.py +0 -0
  84. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/errors.py +0 -0
  85. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/graph_query.py +0 -0
  86. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/recorded.py +0 -0
  87. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/tools/web_fetch.py +0 -0
  88. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/trace/__init__.py +0 -0
  89. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/trace/causal.py +0 -0
  90. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph/trace/printer.py +0 -0
  91. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph.egg-info/SOURCES.txt +0 -0
  92. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph.egg-info/dependency_links.txt +0 -0
  93. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph.egg-info/entry_points.txt +0 -0
  94. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph.egg-info/requires.txt +0 -0
  95. {activegraph-1.0.3 → activegraph-1.0.4}/activegraph.egg-info/top_level.txt +0 -0
  96. {activegraph-1.0.3 → activegraph-1.0.4}/setup.cfg +0 -0
  97. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_activate_after.py +0 -0
  98. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_causal_cross_tool.py +0 -0
  99. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_cli.py +0 -0
  100. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_clock.py +0 -0
  101. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_diff.py +0 -0
  102. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_diligence_pack.py +0 -0
  103. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_diligence_with_tools.py +0 -0
  104. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_doc_links.py +0 -0
  105. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_doc_site_reachable.py +0 -0
  106. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_errors_format.py +0 -0
  107. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_event.py +0 -0
  108. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_fork.py +0 -0
  109. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_ids.py +0 -0
  110. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_anthropic.py +0 -0
  111. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_behavior.py +0 -0
  112. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_budget.py +0 -0
  113. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_causal.py +0 -0
  114. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_claim_extraction.py +0 -0
  115. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_default_model.py +0 -0
  116. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_determinism.py +0 -0
  117. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_failure.py +0 -0
  118. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_openai.py +0 -0
  119. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_prompt.py +0 -0
  120. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_provider_fixtures.py +0 -0
  121. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_replay.py +0 -0
  122. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_tool_loop.py +0 -0
  123. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_trace.py +0 -0
  124. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_trace_snapshot.py +0 -0
  125. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_llm_types.py +0 -0
  126. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_migration.py +0 -0
  127. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_observability_logging.py +0 -0
  128. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_observability_metrics.py +0 -0
  129. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_operate_example.py +0 -0
  130. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_pack_scaffold.py +0 -0
  131. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_packs.py +0 -0
  132. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_patch.py +0 -0
  133. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_pattern_matcher.py +0 -0
  134. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_pattern_parser.py +0 -0
  135. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_pattern_subscriptions.py +0 -0
  136. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_persistence.py +0 -0
  137. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_postgres_store.py +0 -0
  138. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_quickstart.py +0 -0
  139. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_quickstart_snapshot.py +0 -0
  140. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_replay.py +0 -0
  141. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_replay_trace_snapshot.py +0 -0
  142. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_resume_example.py +0 -0
  143. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_runtime.py +0 -0
  144. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_runtime_status.py +0 -0
  145. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_serde.py +0 -0
  146. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_store_conformance.py +0 -0
  147. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_store_url.py +0 -0
  148. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_tool_replay.py +0 -0
  149. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_tool_trace_snapshot.py +0 -0
  150. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_tools.py +0 -0
  151. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_trace.py +0 -0
  152. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_tutorial_snippets.py +0 -0
  153. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_v1_0_1_patches.py +0 -0
  154. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  155. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  156. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_version_sync.py +0 -0
  157. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_view.py +0 -0
  158. {activegraph-1.0.3 → activegraph-1.0.4}/tests/test_wheel_completeness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: An event-sourced reactive graph runtime for long-running, auditable, agentic systems.
5
5
  Author: Active Graph contributors
6
6
  License: MIT
@@ -222,4 +222,4 @@ __all__ = [
222
222
  "tool",
223
223
  ]
224
224
 
225
- __version__ = "1.0.3"
225
+ __version__ = "1.0.4"
@@ -181,6 +181,33 @@ class Graph:
181
181
  out.append(r)
182
182
  return out
183
183
 
184
+ def relations(
185
+ self,
186
+ source: Optional[str] = None,
187
+ target: Optional[str] = None,
188
+ type: Optional[str] = None,
189
+ ) -> list[Relation]:
190
+ """Return relations filtered by ``source``, ``target``, and/or ``type``.
191
+
192
+ v1.0.4 #1: the canonical filter API on ``Graph``. Decomposes the
193
+ v0 ``get_relations(object_id=, direction=)`` axis into separate
194
+ ``source`` and ``target`` slots so the call reads the way users
195
+ already write it (matches ``docs/concepts/graph.md``). Filter
196
+ kwargs compose by AND; calling with no kwargs returns every
197
+ relation. ``Graph.get_relations(object_id=, type=, direction=)``
198
+ stays as a backward-compatible alias.
199
+ """
200
+ out: list[Relation] = []
201
+ for r in self._relations.values():
202
+ if source is not None and r.source != source:
203
+ continue
204
+ if target is not None and r.target != target:
205
+ continue
206
+ if type is not None and r.type != type:
207
+ continue
208
+ out.append(r)
209
+ return out
210
+
184
211
  def neighborhood(self, object_id: str, depth: int = 1) -> tuple[list[Object], list[Relation]]:
185
212
  if object_id not in self._objects:
186
213
  return ([], [])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: An event-sourced reactive graph runtime for long-running, auditable, agentic systems.
5
5
  Author: Active Graph contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "activegraph"
7
- version = "1.0.3"
7
+ version = "1.0.4"
8
8
  description = "An event-sourced reactive graph runtime for long-running, auditable, agentic systems."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -129,6 +129,104 @@ def test_query_alias_still_works_with_positional_arg():
129
129
  assert {o.type for o in g.query("claim")} == {"claim"}
130
130
 
131
131
 
132
+ # v1.0.4 #1: graph.relations(source=, target=, type=) as the canonical
133
+ # filter API, decomposing the v0 get_relations(object_id=, direction=)
134
+ # axis into separate source/target slots. The contract claim is the
135
+ # eight filter combinations enumerated in CONTRACT v1.0.4 #1; the tests
136
+ # anchor on each combination directly, not on equivalence to the alias.
137
+ # The alias-still-works check is its own test.
138
+
139
+
140
+ def _three_node_graph():
141
+ g = _g()
142
+ a = g.add_object("task", {"k": "a"})
143
+ b = g.add_object("task", {"k": "b"})
144
+ c = g.add_object("task", {"k": "c"})
145
+ rel_ab_dep = g.add_relation(a.id, b.id, "depends_on")
146
+ rel_ac_dep = g.add_relation(a.id, c.id, "depends_on")
147
+ rel_bc_blk = g.add_relation(b.id, c.id, "blocks")
148
+ rel_ba_dep = g.add_relation(b.id, a.id, "depends_on")
149
+ return g, a, b, c, rel_ab_dep, rel_ac_dep, rel_bc_blk, rel_ba_dep
150
+
151
+
152
+ def test_relations_no_kwargs_returns_every_relation():
153
+ # Contract row 1: (None, None, None) -> every relation.
154
+ g, _, _, _, *rels = _three_node_graph()
155
+ out = g.relations()
156
+ assert {r.id for r in out} == {r.id for r in rels}
157
+
158
+
159
+ def test_relations_source_only_returns_outgoing_from_source():
160
+ # Contract row 2: (A, None, None) -> relations where source == A.
161
+ g, a, b, c, rel_ab, rel_ac, _, _ = _three_node_graph()
162
+ out = g.relations(source=a.id)
163
+ assert {r.id for r in out} == {rel_ab.id, rel_ac.id}
164
+ assert all(r.source == a.id for r in out)
165
+
166
+
167
+ def test_relations_target_only_returns_incoming_to_target():
168
+ # Contract row 3: (None, B, None) -> relations where target == B.
169
+ g, a, b, c, rel_ab, _, _, _ = _three_node_graph()
170
+ out = g.relations(target=b.id)
171
+ assert {r.id for r in out} == {rel_ab.id}
172
+ assert all(r.target == b.id for r in out)
173
+
174
+
175
+ def test_relations_source_and_target_returns_intersection():
176
+ # Contract row 4: (A, B, None) -> relations from A to B.
177
+ g, a, b, c, rel_ab, _, _, rel_ba = _three_node_graph()
178
+ out = g.relations(source=a.id, target=b.id)
179
+ assert {r.id for r in out} == {rel_ab.id}
180
+ # Not the reverse direction.
181
+ assert rel_ba.id not in {r.id for r in out}
182
+
183
+
184
+ def test_relations_type_only_returns_every_relation_of_type():
185
+ # Contract row 5: (None, None, T) -> every relation of type T.
186
+ g, *_, rel_bc_blk, _ = _three_node_graph()
187
+ out = g.relations(type="blocks")
188
+ assert {r.id for r in out} == {rel_bc_blk.id}
189
+ assert all(r.type == "blocks" for r in out)
190
+
191
+
192
+ def test_relations_source_and_type_returns_outgoing_of_type():
193
+ # Contract row 6: (A, None, T) -> relations from A of type T.
194
+ g, a, _, _, rel_ab, rel_ac, _, _ = _three_node_graph()
195
+ out = g.relations(source=a.id, type="depends_on")
196
+ assert {r.id for r in out} == {rel_ab.id, rel_ac.id}
197
+
198
+
199
+ def test_relations_target_and_type_returns_incoming_of_type():
200
+ # Contract row 7: (None, B, T) -> relations to B of type T.
201
+ g, a, b, _, rel_ab, _, _, _ = _three_node_graph()
202
+ out = g.relations(target=b.id, type="depends_on")
203
+ assert {r.id for r in out} == {rel_ab.id}
204
+
205
+
206
+ def test_relations_source_target_and_type_most_specific():
207
+ # Contract row 8: (A, B, T) -> relations from A to B of type T.
208
+ g, a, b, _, rel_ab, _, _, rel_ba = _three_node_graph()
209
+ out = g.relations(source=a.id, target=b.id, type="depends_on")
210
+ assert {r.id for r in out} == {rel_ab.id}
211
+ # Different-direction same-type relation is excluded.
212
+ assert rel_ba.id not in {r.id for r in out}
213
+ # Wrong-type from A->B does not exist; sanity check empty for a wrong type.
214
+ assert g.relations(source=a.id, target=b.id, type="blocks") == []
215
+
216
+
217
+ def test_get_relations_alias_still_works():
218
+ # Backward-compat: the v0 shape continues to work byte-identically.
219
+ # This is the alias relationship, not the contract claim — kept as
220
+ # its own test per CONTRACT v1.0.4 #1's Standing Rule §2 carve-out.
221
+ g, a, b, c, rel_ab, rel_ac, _, rel_ba = _three_node_graph()
222
+ out = g.get_relations(object_id=a.id, direction="outgoing")
223
+ assert {r.id for r in out} == {rel_ab.id, rel_ac.id}
224
+ incoming = g.get_relations(object_id=a.id, direction="incoming")
225
+ assert {r.id for r in incoming} == {rel_ba.id}
226
+ both = g.get_relations(object_id=a.id, direction="both")
227
+ assert {r.id for r in both} == {rel_ab.id, rel_ac.id, rel_ba.id}
228
+
229
+
132
230
  def test_neighborhood_walks_to_depth():
133
231
  g = _g()
134
232
  a = g.add_object("task", {})
@@ -128,6 +128,70 @@ class TestRequeueUnfiredDoesNotFalselyRequeue:
128
128
  except FileNotFoundError:
129
129
  pass
130
130
 
131
+ def test_zero_subscriber_event_ids_are_absent_from_requeue_set_on_load(self):
132
+ """Boundary-anchored sibling to the queue_depth assertion above.
133
+
134
+ CONTRACT v0.5 #8 names the boundary as `Runtime.load` and the
135
+ subject as the requeue set itself: "events with NO
136
+ `behavior.started` referencing them are re-queued; events that
137
+ any behavior started on are NOT re-queued." A zero-subscriber
138
+ event has no `behavior.started` referencing it but is
139
+ popped-and-discarded by the runtime loop — so the contract
140
+ carve-out is that zero-subscriber events are NOT in the
141
+ requeue set on load.
142
+
143
+ The sibling test above asserts `queue_depth == 0` (the
144
+ implementation's symptom). This test asserts directly on the
145
+ boundary the contract names: it identifies the zero-subscriber
146
+ event IDs from the saved log, reloads, and verifies those IDs
147
+ are absent from `rt._queue` after `_requeue_unfired` ran on
148
+ load. Anchors on what the contract says, not on whether the
149
+ post-load queue depth happens to be zero (CONTRACT v1.0.4 #4,
150
+ Standing Rule §2).
151
+ """
152
+ _register_subscriber_for_goal_only()
153
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
154
+ path = f.name
155
+ os.remove(path)
156
+ try:
157
+ g = Graph()
158
+ rt = Runtime(g, persist_to=path)
159
+ rt.run_goal("test_goal")
160
+ rt.save_state()
161
+
162
+ zero_subscriber_event_ids = {
163
+ e.id for e in rt.graph.events
164
+ if e.type == "downstream.no_subscribers"
165
+ }
166
+ assert zero_subscriber_event_ids, (
167
+ "Fixture is supposed to produce at least one "
168
+ "downstream.no_subscribers event. Without it, the "
169
+ "contract claim's specific subject (the zero-"
170
+ "subscriber carve-out) isn't being exercised."
171
+ )
172
+
173
+ _register_subscriber_for_goal_only()
174
+ rt2 = Runtime.load(path, run_id=rt.run_id)
175
+
176
+ requeued_event_ids = {e.id for e in rt2._queue._q} # noqa: SLF001
177
+ falsely_requeued = zero_subscriber_event_ids & requeued_event_ids
178
+ assert not falsely_requeued, (
179
+ f"CONTRACT v0.5 #8 carve-out: zero-subscriber events are "
180
+ f"popped-and-discarded with no behavior.started emitted, "
181
+ f"so on Runtime.load they must NOT appear in the requeue "
182
+ f"set. The following zero-subscriber event IDs were "
183
+ f"falsely requeued: {sorted(falsely_requeued)}. This is "
184
+ f"the v0.5 #8 boundary the contract names; failure here "
185
+ f"means the requeue set itself violates the contract "
186
+ f"regardless of what queue_depth summarizes."
187
+ )
188
+ finally:
189
+ for suffix in ("", "-wal", "-shm"):
190
+ try:
191
+ os.remove(path + suffix)
192
+ except FileNotFoundError:
193
+ pass
194
+
131
195
 
132
196
  class TestRequeueUnfiredPreservesCrashRecovery:
133
197
  """The fix narrows the requeue scope to events after the last drain.
File without changes
File without changes