spanweave 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 (246) hide show
  1. spanweave-0.9.0/.github/workflows/ci.yml +64 -0
  2. spanweave-0.9.0/.gitignore +42 -0
  3. spanweave-0.9.0/ADAPTERS.md +293 -0
  4. spanweave-0.9.0/AGENT.md +391 -0
  5. spanweave-0.9.0/CLAUDE.md +115 -0
  6. spanweave-0.9.0/CONTRACTS.md +638 -0
  7. spanweave-0.9.0/CONTRIBUTING.md +92 -0
  8. spanweave-0.9.0/DESIGN.md +193 -0
  9. spanweave-0.9.0/ENVIRONMENT.md +139 -0
  10. spanweave-0.9.0/FIXTURES.md +567 -0
  11. spanweave-0.9.0/GLOSSARY.md +100 -0
  12. spanweave-0.9.0/LICENSE +21 -0
  13. spanweave-0.9.0/Makefile +104 -0
  14. spanweave-0.9.0/OPEN_QUESTIONS.md +407 -0
  15. spanweave-0.9.0/PKG-INFO +301 -0
  16. spanweave-0.9.0/PREDICTIONS.md +673 -0
  17. spanweave-0.9.0/README.md +275 -0
  18. spanweave-0.9.0/ROADMAP.md +458 -0
  19. spanweave-0.9.0/SECURITY.md +81 -0
  20. spanweave-0.9.0/SPEC.md +850 -0
  21. spanweave-0.9.0/TASKS.md +8192 -0
  22. spanweave-0.9.0/capture/README.md +490 -0
  23. spanweave-0.9.0/capture/__init__.py +12 -0
  24. spanweave-0.9.0/capture/backends.py +1385 -0
  25. spanweave-0.9.0/capture/exporter.py +153 -0
  26. spanweave-0.9.0/capture/fleet.py +389 -0
  27. spanweave-0.9.0/capture/run.py +626 -0
  28. spanweave-0.9.0/examples/__init__.py +1 -0
  29. spanweave-0.9.0/examples/cost_latency/__init__.py +981 -0
  30. spanweave-0.9.0/examples/cost_latency/__main__.py +352 -0
  31. spanweave-0.9.0/examples/cost_latency/load.py +105 -0
  32. spanweave-0.9.0/examples/fleet_aggregate/__init__.py +247 -0
  33. spanweave-0.9.0/examples/fleet_aggregate/__main__.py +139 -0
  34. spanweave-0.9.0/examples/trajectory_dump/__init__.py +588 -0
  35. spanweave-0.9.0/examples/trajectory_dump/__main__.py +211 -0
  36. spanweave-0.9.0/fixtures/captured/README.md +32 -0
  37. spanweave-0.9.0/fixtures/captured/genai_tool_call.jsonl +4 -0
  38. spanweave-0.9.0/fixtures/captured/genai_tool_call.provenance.md +87 -0
  39. spanweave-0.9.0/fixtures/captured/genai_workflow.jsonl +9 -0
  40. spanweave-0.9.0/fixtures/captured/genai_workflow.provenance.md +107 -0
  41. spanweave-0.9.0/fixtures/captured/openai_tool_call.jsonl +4 -0
  42. spanweave-0.9.0/fixtures/captured/openai_tool_call.provenance.md +94 -0
  43. spanweave-0.9.0/fixtures/conformance/README.md +70 -0
  44. spanweave-0.9.0/fixtures/conformance/clock_skew/dialects/openinference.jsonl +3 -0
  45. spanweave-0.9.0/fixtures/conformance/clock_skew/dialects/otel_genai.jsonl +3 -0
  46. spanweave-0.9.0/fixtures/conformance/clock_skew/expected/comparison.json +6 -0
  47. spanweave-0.9.0/fixtures/conformance/clock_skew/expected/diagnostics.json +13 -0
  48. spanweave-0.9.0/fixtures/conformance/clock_skew/expected/graph.json +100 -0
  49. spanweave-0.9.0/fixtures/conformance/clock_skew/otel_genai.notes.md +19 -0
  50. spanweave-0.9.0/fixtures/conformance/clock_skew/scenario.md +54 -0
  51. spanweave-0.9.0/fixtures/conformance/cyclic_parents/dialects/openinference.jsonl +2 -0
  52. spanweave-0.9.0/fixtures/conformance/cyclic_parents/expected/coverage.json +7 -0
  53. spanweave-0.9.0/fixtures/conformance/cyclic_parents/expected/diagnostics.json +9 -0
  54. spanweave-0.9.0/fixtures/conformance/cyclic_parents/expected/graph.json +77 -0
  55. spanweave-0.9.0/fixtures/conformance/cyclic_parents/scenario.md +62 -0
  56. spanweave-0.9.0/fixtures/conformance/declared_data_edge/dialects/openinference.jsonl +2 -0
  57. spanweave-0.9.0/fixtures/conformance/declared_data_edge/dialects/otel_genai.jsonl +2 -0
  58. spanweave-0.9.0/fixtures/conformance/declared_data_edge/expected/comparison.json +17 -0
  59. spanweave-0.9.0/fixtures/conformance/declared_data_edge/expected/diagnostics.json +13 -0
  60. spanweave-0.9.0/fixtures/conformance/declared_data_edge/expected/graph.json +117 -0
  61. spanweave-0.9.0/fixtures/conformance/declared_data_edge/expected/payloads/otel_genai.json +53 -0
  62. spanweave-0.9.0/fixtures/conformance/declared_data_edge/otel_genai.notes.md +32 -0
  63. spanweave-0.9.0/fixtures/conformance/declared_data_edge/scenario.md +84 -0
  64. spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/dialects/openinference.jsonl +2 -0
  65. spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/dialects/otel_genai.jsonl +2 -0
  66. spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/expected/error.json +5 -0
  67. spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/otel_genai.notes.md +13 -0
  68. spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/scenario.md +41 -0
  69. spanweave-0.9.0/fixtures/conformance/empty_payload/dialects/openinference.jsonl +1 -0
  70. spanweave-0.9.0/fixtures/conformance/empty_payload/dialects/otel_genai.jsonl +1 -0
  71. spanweave-0.9.0/fixtures/conformance/empty_payload/expected/comparison.json +15 -0
  72. spanweave-0.9.0/fixtures/conformance/empty_payload/expected/diagnostics.json +4 -0
  73. spanweave-0.9.0/fixtures/conformance/empty_payload/expected/graph.json +34 -0
  74. spanweave-0.9.0/fixtures/conformance/empty_payload/expected/payloads/otel_genai.json +7 -0
  75. spanweave-0.9.0/fixtures/conformance/empty_payload/otel_genai.notes.md +37 -0
  76. spanweave-0.9.0/fixtures/conformance/empty_payload/scenario.md +43 -0
  77. spanweave-0.9.0/fixtures/conformance/llm_tool_llm/dialects/openinference.jsonl +4 -0
  78. spanweave-0.9.0/fixtures/conformance/llm_tool_llm/dialects/otel_genai.jsonl +4 -0
  79. spanweave-0.9.0/fixtures/conformance/llm_tool_llm/expected/comparison.json +27 -0
  80. spanweave-0.9.0/fixtures/conformance/llm_tool_llm/expected/diagnostics.json +9 -0
  81. spanweave-0.9.0/fixtures/conformance/llm_tool_llm/expected/graph.json +229 -0
  82. spanweave-0.9.0/fixtures/conformance/llm_tool_llm/expected/payloads/otel_genai.json +99 -0
  83. spanweave-0.9.0/fixtures/conformance/llm_tool_llm/otel_genai.notes.md +110 -0
  84. spanweave-0.9.0/fixtures/conformance/llm_tool_llm/scenario.md +134 -0
  85. spanweave-0.9.0/fixtures/conformance/malformed_payload_json/dialects/openinference.jsonl +1 -0
  86. spanweave-0.9.0/fixtures/conformance/malformed_payload_json/dialects/otel_genai.jsonl +1 -0
  87. spanweave-0.9.0/fixtures/conformance/malformed_payload_json/expected/comparison.json +6 -0
  88. spanweave-0.9.0/fixtures/conformance/malformed_payload_json/expected/diagnostics.json +9 -0
  89. spanweave-0.9.0/fixtures/conformance/malformed_payload_json/expected/graph.json +39 -0
  90. spanweave-0.9.0/fixtures/conformance/malformed_payload_json/otel_genai.notes.md +26 -0
  91. spanweave-0.9.0/fixtures/conformance/malformed_payload_json/scenario.md +52 -0
  92. spanweave-0.9.0/fixtures/conformance/missing_payloads/dialects/openinference.jsonl +2 -0
  93. spanweave-0.9.0/fixtures/conformance/missing_payloads/dialects/otel_genai.jsonl +2 -0
  94. spanweave-0.9.0/fixtures/conformance/missing_payloads/expected/comparison.json +6 -0
  95. spanweave-0.9.0/fixtures/conformance/missing_payloads/expected/diagnostics.json +4 -0
  96. spanweave-0.9.0/fixtures/conformance/missing_payloads/expected/graph.json +63 -0
  97. spanweave-0.9.0/fixtures/conformance/missing_payloads/otel_genai.notes.md +30 -0
  98. spanweave-0.9.0/fixtures/conformance/missing_payloads/scenario.md +38 -0
  99. spanweave-0.9.0/fixtures/conformance/nested_agents/dialects/openinference.jsonl +3 -0
  100. spanweave-0.9.0/fixtures/conformance/nested_agents/dialects/otel_genai.jsonl +3 -0
  101. spanweave-0.9.0/fixtures/conformance/nested_agents/expected/comparison.json +6 -0
  102. spanweave-0.9.0/fixtures/conformance/nested_agents/expected/diagnostics.json +4 -0
  103. spanweave-0.9.0/fixtures/conformance/nested_agents/expected/graph.json +91 -0
  104. spanweave-0.9.0/fixtures/conformance/nested_agents/otel_genai.notes.md +22 -0
  105. spanweave-0.9.0/fixtures/conformance/nested_agents/scenario.md +42 -0
  106. spanweave-0.9.0/fixtures/conformance/orphan_parent/dialects/openinference.jsonl +1 -0
  107. spanweave-0.9.0/fixtures/conformance/orphan_parent/dialects/otel_genai.jsonl +1 -0
  108. spanweave-0.9.0/fixtures/conformance/orphan_parent/expected/comparison.json +6 -0
  109. spanweave-0.9.0/fixtures/conformance/orphan_parent/expected/diagnostics.json +9 -0
  110. spanweave-0.9.0/fixtures/conformance/orphan_parent/expected/graph.json +39 -0
  111. spanweave-0.9.0/fixtures/conformance/orphan_parent/otel_genai.notes.md +12 -0
  112. spanweave-0.9.0/fixtures/conformance/orphan_parent/scenario.md +43 -0
  113. spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/dialects/openinference.jsonl +4 -0
  114. spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/dialects/otel_genai.jsonl +4 -0
  115. spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/expected/comparison.json +21 -0
  116. spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/expected/diagnostics.json +9 -0
  117. spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/expected/graph.json +184 -0
  118. spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/expected/payloads/otel_genai.json +56 -0
  119. spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/otel_genai.notes.md +71 -0
  120. spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/scenario.md +84 -0
  121. spanweave-0.9.0/fixtures/conformance/parallel_tools/dialects/openinference.jsonl +4 -0
  122. spanweave-0.9.0/fixtures/conformance/parallel_tools/dialects/otel_genai.jsonl +4 -0
  123. spanweave-0.9.0/fixtures/conformance/parallel_tools/expected/comparison.json +15 -0
  124. spanweave-0.9.0/fixtures/conformance/parallel_tools/expected/diagnostics.json +4 -0
  125. spanweave-0.9.0/fixtures/conformance/parallel_tools/expected/graph.json +133 -0
  126. spanweave-0.9.0/fixtures/conformance/parallel_tools/expected/payloads/otel_genai.json +17 -0
  127. spanweave-0.9.0/fixtures/conformance/parallel_tools/otel_genai.notes.md +25 -0
  128. spanweave-0.9.0/fixtures/conformance/parallel_tools/scenario.md +64 -0
  129. spanweave-0.9.0/fixtures/conformance/redacted_payload/dialects/openinference.jsonl +1 -0
  130. spanweave-0.9.0/fixtures/conformance/redacted_payload/expected/coverage.json +7 -0
  131. spanweave-0.9.0/fixtures/conformance/redacted_payload/expected/diagnostics.json +4 -0
  132. spanweave-0.9.0/fixtures/conformance/redacted_payload/expected/graph.json +37 -0
  133. spanweave-0.9.0/fixtures/conformance/redacted_payload/scenario.md +68 -0
  134. spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/dialects/openinference.jsonl +3 -0
  135. spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/expected/coverage.json +7 -0
  136. spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/expected/diagnostics.json +4 -0
  137. spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/expected/graph.json +110 -0
  138. spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/scenario.md +109 -0
  139. spanweave-0.9.0/fixtures/conformance/shuffled_order/dialects/openinference.jsonl +4 -0
  140. spanweave-0.9.0/fixtures/conformance/shuffled_order/dialects/otel_genai.jsonl +4 -0
  141. spanweave-0.9.0/fixtures/conformance/shuffled_order/expected/comparison.json +27 -0
  142. spanweave-0.9.0/fixtures/conformance/shuffled_order/expected/diagnostics.json +9 -0
  143. spanweave-0.9.0/fixtures/conformance/shuffled_order/expected/graph.json +229 -0
  144. spanweave-0.9.0/fixtures/conformance/shuffled_order/expected/payloads/otel_genai.json +99 -0
  145. spanweave-0.9.0/fixtures/conformance/shuffled_order/otel_genai.notes.md +14 -0
  146. spanweave-0.9.0/fixtures/conformance/shuffled_order/scenario.md +45 -0
  147. spanweave-0.9.0/fixtures/conformance/single_tool_call/dialects/openinference.jsonl +1 -0
  148. spanweave-0.9.0/fixtures/conformance/single_tool_call/dialects/otel_genai.jsonl +1 -0
  149. spanweave-0.9.0/fixtures/conformance/single_tool_call/expected/comparison.json +6 -0
  150. spanweave-0.9.0/fixtures/conformance/single_tool_call/expected/diagnostics.json +4 -0
  151. spanweave-0.9.0/fixtures/conformance/single_tool_call/expected/graph.json +38 -0
  152. spanweave-0.9.0/fixtures/conformance/single_tool_call/otel_genai.notes.md +19 -0
  153. spanweave-0.9.0/fixtures/conformance/single_tool_call/scenario.md +41 -0
  154. spanweave-0.9.0/fixtures/conformance/span_links/dialects/openinference.jsonl +2 -0
  155. spanweave-0.9.0/fixtures/conformance/span_links/expected/coverage.json +7 -0
  156. spanweave-0.9.0/fixtures/conformance/span_links/expected/diagnostics.json +4 -0
  157. spanweave-0.9.0/fixtures/conformance/span_links/expected/graph.json +79 -0
  158. spanweave-0.9.0/fixtures/conformance/span_links/scenario.md +77 -0
  159. spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/dialects/openinference.jsonl +3 -0
  160. spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/dialects/otel_genai.jsonl +3 -0
  161. spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/expected/comparison.json +23 -0
  162. spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/expected/diagnostics.json +9 -0
  163. spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/expected/graph.json +174 -0
  164. spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/expected/payloads/otel_genai.json +85 -0
  165. spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/otel_genai.notes.md +89 -0
  166. spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/scenario.md +103 -0
  167. spanweave-0.9.0/fixtures/conformance/unknown_kind/dialects/openinference.jsonl +2 -0
  168. spanweave-0.9.0/fixtures/conformance/unknown_kind/dialects/otel_genai.jsonl +2 -0
  169. spanweave-0.9.0/fixtures/conformance/unknown_kind/expected/comparison.json +7 -0
  170. spanweave-0.9.0/fixtures/conformance/unknown_kind/expected/diagnostics.json +9 -0
  171. spanweave-0.9.0/fixtures/conformance/unknown_kind/expected/graph.json +69 -0
  172. spanweave-0.9.0/fixtures/conformance/unknown_kind/otel_genai.notes.md +44 -0
  173. spanweave-0.9.0/fixtures/conformance/unknown_kind/scenario.md +90 -0
  174. spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/dialects/openinference.jsonl +3 -0
  175. spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/dialects/otel_genai.jsonl +3 -0
  176. spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/expected/comparison.json +17 -0
  177. spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/expected/diagnostics.json +17 -0
  178. spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/expected/graph.json +137 -0
  179. spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/expected/payloads/otel_genai.json +34 -0
  180. spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/otel_genai.notes.md +134 -0
  181. spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/scenario.md +66 -0
  182. spanweave-0.9.0/fixtures/conformance/unset_and_error_status/dialects/openinference.jsonl +3 -0
  183. spanweave-0.9.0/fixtures/conformance/unset_and_error_status/dialects/otel_genai.jsonl +3 -0
  184. spanweave-0.9.0/fixtures/conformance/unset_and_error_status/expected/comparison.json +6 -0
  185. spanweave-0.9.0/fixtures/conformance/unset_and_error_status/expected/diagnostics.json +4 -0
  186. spanweave-0.9.0/fixtures/conformance/unset_and_error_status/expected/graph.json +100 -0
  187. spanweave-0.9.0/fixtures/conformance/unset_and_error_status/provenance.notes.md +56 -0
  188. spanweave-0.9.0/fixtures/conformance/unset_and_error_status/scenario.md +71 -0
  189. spanweave-0.9.0/pyproject.toml +107 -0
  190. spanweave-0.9.0/spanweave/__init__.py +77 -0
  191. spanweave-0.9.0/spanweave/adapters/__init__.py +161 -0
  192. spanweave-0.9.0/spanweave/adapters/base.py +58 -0
  193. spanweave-0.9.0/spanweave/adapters/openinference.py +544 -0
  194. spanweave-0.9.0/spanweave/adapters/otel_genai.py +709 -0
  195. spanweave-0.9.0/spanweave/annotate.py +132 -0
  196. spanweave-0.9.0/spanweave/api.py +43 -0
  197. spanweave-0.9.0/spanweave/build.py +595 -0
  198. spanweave-0.9.0/spanweave/cli.py +264 -0
  199. spanweave-0.9.0/spanweave/diagnostics.py +116 -0
  200. spanweave-0.9.0/spanweave/errors.py +97 -0
  201. spanweave-0.9.0/spanweave/graph.py +321 -0
  202. spanweave-0.9.0/spanweave/ids.py +97 -0
  203. spanweave-0.9.0/spanweave/model.py +319 -0
  204. spanweave-0.9.0/spanweave/read.py +185 -0
  205. spanweave-0.9.0/spanweave/seam.py +142 -0
  206. spanweave-0.9.0/spanweave/serialize.py +293 -0
  207. spanweave-0.9.0/spanweave/version.py +19 -0
  208. spanweave-0.9.0/tests/__init__.py +0 -0
  209. spanweave-0.9.0/tests/conformance.py +357 -0
  210. spanweave-0.9.0/tests/conftest.py +73 -0
  211. spanweave-0.9.0/tests/determinism.py +128 -0
  212. spanweave-0.9.0/tests/gates.py +403 -0
  213. spanweave-0.9.0/tests/install_check.py +928 -0
  214. spanweave-0.9.0/tests/readme_quickstart.py +119 -0
  215. spanweave-0.9.0/tests/schema_shape.py +389 -0
  216. spanweave-0.9.0/tests/serialized_shape.json +225 -0
  217. spanweave-0.9.0/tests/stranger_path.py +212 -0
  218. spanweave-0.9.0/tests/test_acceptance.py +202 -0
  219. spanweave-0.9.0/tests/test_adapters.py +223 -0
  220. spanweave-0.9.0/tests/test_build.py +522 -0
  221. spanweave-0.9.0/tests/test_capture.py +2026 -0
  222. spanweave-0.9.0/tests/test_cli.py +157 -0
  223. spanweave-0.9.0/tests/test_codes.py +286 -0
  224. spanweave-0.9.0/tests/test_conformance.py +946 -0
  225. spanweave-0.9.0/tests/test_contracts.py +459 -0
  226. spanweave-0.9.0/tests/test_detection.py +204 -0
  227. spanweave-0.9.0/tests/test_determinism.py +242 -0
  228. spanweave-0.9.0/tests/test_doc_truth.py +669 -0
  229. spanweave-0.9.0/tests/test_docs.py +107 -0
  230. spanweave-0.9.0/tests/test_example_cost_latency.py +647 -0
  231. spanweave-0.9.0/tests/test_example_fleet_aggregate.py +241 -0
  232. spanweave-0.9.0/tests/test_example_trajectory_dump.py +505 -0
  233. spanweave-0.9.0/tests/test_gates.py +228 -0
  234. spanweave-0.9.0/tests/test_graph.py +380 -0
  235. spanweave-0.9.0/tests/test_ids.py +134 -0
  236. spanweave-0.9.0/tests/test_model.py +285 -0
  237. spanweave-0.9.0/tests/test_openinference.py +611 -0
  238. spanweave-0.9.0/tests/test_ordering.py +291 -0
  239. spanweave-0.9.0/tests/test_otel_genai.py +659 -0
  240. spanweave-0.9.0/tests/test_prediction_evidence.py +331 -0
  241. spanweave-0.9.0/tests/test_read.py +145 -0
  242. spanweave-0.9.0/tests/test_readme_quickstart.py +195 -0
  243. spanweave-0.9.0/tests/test_schema_shape.py +246 -0
  244. spanweave-0.9.0/tests/test_serialize.py +173 -0
  245. spanweave-0.9.0/tests/test_version.py +71 -0
  246. spanweave-0.9.0/uv.lock +399 -0
@@ -0,0 +1,64 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ # setup-uv resolves its version by fetching a manifest from
8
+ # raw.githubusercontent.com. That fetch is a known intermittent failure
9
+ # (astral-sh/setup-uv#849, #869). Passing github-token authenticates it.
10
+ # The token is provided automatically by Actions -- nothing to create.
11
+ # If this still flakes, drop the action entirely and use:
12
+ # run: curl -LsSf https://astral.sh/uv/install.sh | sh
13
+ # and update ENVIRONMENT.md to match rather than leaving a stale reference.
14
+
15
+ jobs:
16
+ check:
17
+ runs-on: ubuntu-latest
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ # A library that sits underneath other people's tools must not narrow
22
+ # their runtime (ENVIRONMENT.md).
23
+ python-version: ["3.11", "3.12", "3.13"]
24
+ steps:
25
+ - uses: actions/checkout@v6
26
+ - name: Install uv
27
+ # @v8 / @v8.0 moving tags no longer resolve; pin a full version tag.
28
+ uses: astral-sh/setup-uv@v8.1.0
29
+ with:
30
+ version: "0.11.25"
31
+ github-token: ${{ secrets.GITHUB_TOKEN }}
32
+ - run: uv sync --extra dev --python ${{ matrix.python-version }}
33
+ # make check wraps the exact toolchain commands (ruff, mypy, pytest)
34
+ # plus the invariant gates and phase done-whens -- the acceptance
35
+ # harness, task 0.8.
36
+ - run: make check
37
+ # make check runs everything under `uv run`, with the source tree on the
38
+ # path -- so it can only answer questions about the repository. This is
39
+ # the only gate that builds the distribution, installs the wheel into a
40
+ # throwaway venv and runs it from outside the repo (TASKS.md 3.6). A
41
+ # packaging break passes every other gate and fails for the first
42
+ # stranger. It reaches PyPI only as the step above does, for
43
+ # dependencies: ENVIRONMENT.md network zone 1. It never publishes.
44
+ - run: make install-check
45
+
46
+ determinism:
47
+ # Determinism is a cross-machine claim (CLAUDE.md 4), so it is worth
48
+ # proving on more than one OS rather than only on the machine that
49
+ # generated the expected outputs.
50
+ runs-on: ${{ matrix.os }}
51
+ strategy:
52
+ fail-fast: false
53
+ matrix:
54
+ os: [ubuntu-latest, macos-latest]
55
+ steps:
56
+ - uses: actions/checkout@v6
57
+ - name: Install uv
58
+ uses: astral-sh/setup-uv@v8.1.0
59
+ with:
60
+ version: "0.11.25"
61
+ github-token: ${{ secrets.GITHUB_TOKEN }}
62
+ - run: uv sync --extra dev
63
+ - run: make conformance
64
+ - run: uv run pytest tests/test_determinism.py -v
@@ -0,0 +1,42 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+
13
+ # uv: uv.lock IS committed (reproducibility invariant, ENVIRONMENT.md) --
14
+ # do not ignore it.
15
+
16
+ # Tooling caches
17
+ .mypy_cache/
18
+ .ruff_cache/
19
+ .pytest_cache/
20
+ .coverage
21
+ htmlcov/
22
+
23
+ # OS / editor
24
+ .DS_Store
25
+ *.swp
26
+ .idea/
27
+ .vscode/
28
+
29
+ # Secrets -- never commit. Capture-harness API keys live only in the human env.
30
+ .env
31
+ *.key
32
+
33
+ # CLI output (`spanweave build -o out/...`). Generated, deterministic, and
34
+ # reproducible from the committed fixtures -- not source.
35
+ out/
36
+
37
+ # Scratch captures from capture-harness smoke runs. NEVER the authoritative
38
+ # fixtures/captured/*.jsonl -- those are human-reviewed, redacted, and
39
+ # committed on purpose with a provenance file (FIXTURES.md section 6).
40
+ *.local.jsonl
41
+ *.local.json
42
+ capture/_scratch/
@@ -0,0 +1,293 @@
1
+ # ADAPTERS.md — writing an adapter
2
+
3
+ An adapter teaches `spanweave` one telemetry dialect. It is the **only** place
4
+ dialect knowledge is allowed to live, and adding one is the primary way to
5
+ contribute (`CONTRIBUTING.md`).
6
+
7
+ An adapter is a single file plus fixtures. If you find yourself needing to touch
8
+ anything outside `spanweave/adapters/`, **stop** — you have found either a model
9
+ gap or a design error, and both are conversations before they are patches
10
+ (`CLAUDE.md` 6).
11
+
12
+ ## 1. What an adapter does
13
+
14
+ ```
15
+ raw JSON records ──▶ Iterator[NormalizedSpan]
16
+ ```
17
+
18
+ That's it. Adapters do not build graphs, assign ids, create `temporal` edges,
19
+ sort anything, or know that a `Graph` type exists.
20
+
21
+ **Transcribe, don't interpret.** The adapter's job is translation between
22
+ vocabularies. Every time you are tempted to infer something the dialect didn't
23
+ say, the answer is a `Diagnostic` or a `None`.
24
+
25
+ ## 2. The protocol
26
+
27
+ ```python
28
+ class Adapter(Protocol):
29
+ id: str # stable, lowercase, no spaces: "openinference"
30
+ version: str # the adapter's version, independent of the library's
31
+
32
+ def detect(self, sample: Sequence[JsonValue]) -> float: ...
33
+ def parse(self, records: Iterable[JsonValue]) -> Iterator[NormalizedSpan]: ...
34
+ ```
35
+
36
+ ### `detect(sample) -> float`
37
+
38
+ Confidence in `[0.0, 1.0]` that this adapter handles the input. Called with up
39
+ to the first 50 records (`SPEC.md` §6.1).
40
+
41
+ It is a **declaration, not a measurement** — nothing in the trace computes it,
42
+ you are asserting it — and the graph records it under that name
43
+ (`meta.adapters[].declared_confidence`). Score it as something you will have to
44
+ defend to someone reading a graph that came out wrong.
45
+
46
+ - **Must be pure** and **must not raise** — wrap everything.
47
+ - Key on **distinctive marker keys**, not on generic ones. `openinference.span.kind`
48
+ is distinctive; `name` and `start_time` are not.
49
+ - Be honest about partial matches. Return `0.9` when your marker is present,
50
+ `0.3` when the shape is plausible but unmarked, `0.0` when it clearly isn't
51
+ yours. **Do not return `1.0` defensively** — inflated confidence turns
52
+ detection into a race, and a wrong adapter silently producing a plausible
53
+ graph is far worse than an honest "ambiguous input" error.
54
+
55
+ ### `parse(records) -> Iterator[NormalizedSpan]`
56
+
57
+ - **Pure.** No network, no filesystem, no clock, no randomness, no `eval`.
58
+ - **Never raises on malformed input.** Emit what you can and attach a
59
+ diagnostic; skip only when there is genuinely nothing to emit, and say why.
60
+ - **Lazy.** Yield as you go; never materialize the whole input.
61
+ - **Order-independent.** Do not rely on record order for meaning. The builder
62
+ sorts.
63
+
64
+ ## 3. Filling `NormalizedSpan`
65
+
66
+ Field-by-field guidance. The type is defined in `SPEC.md` §6.
67
+
68
+ **Identity**
69
+ - `source_key` — a stable key within this input. Prefer the dialect's span id;
70
+ fall back to the 1-based record index.
71
+ - `span_id` / `parent_id` / `trace_id` — verbatim from the dialect, or `None`.
72
+ **Do not synthesize ids** — that is `spanweave/ids.py`'s job.
73
+
74
+ **Classification**
75
+ - `kind` — map to the closed `NodeKind` set (`SPEC.md` §3.2). If the dialect's
76
+ kind doesn't map, emit `unknown` **and** an `unknown_span_kind` diagnostic
77
+ carrying the original string. Never force a near-miss into a neighbouring kind;
78
+ a wrong kind is worse than an honest `unknown`, because `unknown` is visible
79
+ and a wrong kind isn't.
80
+ - **A value no instrumentor can emit is not a mapping candidate, however well
81
+ it fits the vocabulary.** Map from what instrumentors **produce**, not from
82
+ what the registry **defines**. The two are not the same set, and where they
83
+ differ the registry is the larger one.
84
+
85
+ The test is mechanical: ask what would have to run for a span carrying this
86
+ value to exist. An instrumentor wraps **SDK calls**. If the thing the value
87
+ names is not an SDK call — a workflow, a pipeline stage, a business step —
88
+ then only an *application* can emit it, and every rendering you write for it
89
+ will be derived from a span you wrote yourself. Not just today, while a
90
+ capture is pending: **permanently, for the whole class.** At that point
91
+ `capture/README.md`'s rule binds — *evidence about the outside world that you
92
+ generated from your own idea of the outside world is not evidence* — and
93
+ `unknown` + `reported_kind` is the honest answer. It is a first-class
94
+ outcome, and a consumer that wants the value can read it off the node
95
+ (`SPEC.md` §3.2).
96
+
97
+ This is not a rule about difficulty or about waiting. A mapping whose
98
+ evidence can never arrive is not deferred, it is unfounded, and the
99
+ difference matters because the first looks temporary and the second is not.
100
+
101
+ **The worked case is `otel_genai`'s `invoke_workflow`** (`TASKS.md` 2.16),
102
+ and it is worth reading because the definitional argument for mapping it was
103
+ *real*: `SPEC.md` §3.2 defines `chain` as "a composite step with no more
104
+ specific kind", a workflow is a composite step, and `NodeKind` has nothing
105
+ more specific. It lost to provenance, not to definition. Three conformance
106
+ scenarios pay for that and are declared rather than rendered, and the record
107
+ says so with the cost measured.
108
+
109
+ **When it reopens:** a dialect whose *instrumentor* emits a genuine composite
110
+ step. Then the evidence exists, and the question is a fresh one rather than a
111
+ re-derivation.
112
+ - `name` — as reported. Do not prettify or rewrite.
113
+ - `operation` — the tool/model/retriever name when the dialect distinguishes it
114
+ from `name`.
115
+
116
+ **Payloads** — the part most adapters get wrong.
117
+ - Distinguish all five states (`SPEC.md` §3.3). The distinction between
118
+ **absent** (no attribute emitted) and **empty** (attribute emitted, no
119
+ content) is load-bearing for every downstream consumer, and only you can
120
+ observe it.
121
+ - Parse when the mime type says JSON; on failure keep `raw`, set `value=None`,
122
+ emit `payload_parse_failed`.
123
+ - Preserve `raw` always.
124
+ - Mark `redacted` / `truncated` **only** when the source signals it. Never
125
+ redact or truncate on your own.
126
+ - **A mime the dialect defines but does not emit.** Some dialects carry no
127
+ content-type attribute at all, because their convention *defines* the
128
+ structure of each attribute instead of restating it per span. You **may**
129
+ report that mime and parse accordingly — it is transcribing a fact the
130
+ dialect states about itself, the same class of act as mapping a span-kind
131
+ enum onto a `NodeKind`, and not a guess about an individual span. Three
132
+ conditions, all required:
133
+ 1. **The convention states the structure**, normatively, for that named
134
+ attribute. A convention that says "any" does not qualify, and neither
135
+ does "this instrumentor happens to emit JSON here."
136
+ 2. **A parse failure stays honest** — `state` remains `present`, `value` is
137
+ `None`, `raw` is kept, and `payload_parse_failed` is emitted. If you are
138
+ tempted to suppress that diagnostic, condition 1 was not met.
139
+ 3. **You say so where a reader of the *fixture* will find it**, not only in
140
+ your adapter's docstring: the scenario's cross-dialect notes and, if the
141
+ fixture declares payloads dialect-varying, the `reason` in its
142
+ `expected/comparison.json` (`FIXTURES.md` §4.4). Someone comparing two
143
+ renderings must be able to see why one of them has a mime its dialect
144
+ never wrote, without reading the adapter to find out.
145
+
146
+ The alternative — reporting `mime=None` and leaving `value` as the source
147
+ string — is not the conservative choice it looks like. It makes a payload
148
+ that agrees with another dialect **byte for byte** disagree at model level,
149
+ and the corpus then records a serialization artifact as a finding about the
150
+ model. That is the worse error, because it is the one that looks like
151
+ evidence. `spanweave/adapters/otel_genai.py` is the worked example, and the
152
+ measurement behind it is at `TASKS.md` 2.9.
153
+
154
+ **Usage** — token counts only; no prices, ever (`SPEC.md` §9).
155
+
156
+ **Call pairing** — the highest-value thing you can recover.
157
+ - `call_ids` — the dialect's `tool_call_id` / `function_call_id` / equivalent.
158
+ A **tuple**: one span routinely requests several calls at once, and all of
159
+ them belong in it. Deduplicate, and do not worry about order — the builder
160
+ joins on the ids and sorts what it emits.
161
+ - `call_role` — `requester` on the span that asked, `fulfiller` on the span that
162
+ answered. One role per span, shared by all of its ids.
163
+ - **Take a requester id only from what the span itself produced.** Nearly
164
+ every chat protocol resends the conversation on each turn, so a later span
165
+ carries the earlier turn's call id as *input context*. If you match the id
166
+ wherever it appears, that span becomes a requester and the builder states a
167
+ request-fulfilment relation nobody asserted. Find the part of the dialect
168
+ that distinguishes what the model **said** from what it was **shown** — in
169
+ OpenInference it is `llm.output_messages.*` versus `llm.input_messages.*` —
170
+ and key on it. An echo of a reference is not the reference. Leave the echoed
171
+ ids unmapped so they are reported rather than dropped.
172
+ - If the dialect doesn't carry ids, leave `call_ids` empty and `call_role`
173
+ `None`. **Do not pair by name,
174
+ proximity, or timing** — a guessed pairing is indistinguishable from a real
175
+ one downstream, which is exactly the harm the warrant system exists to
176
+ prevent.
177
+
178
+ **Explicit relations**
179
+ - `links` — span links, when present.
180
+ - `data_edges` — **only** when the dialect explicitly declares a producer→
181
+ consumer relation, naming both ends. Comparing values to guess a flow is
182
+ forbidden (`SPEC.md` §4.2).
183
+ - `received_call_ids` — when the dialect says this span was **given** the
184
+ result of a call (a tool-result message, typically). You cannot name the
185
+ producer from one span, and you should not try: record the id and let the
186
+ builder resolve it (`SPEC.md` §4.2.1).
187
+ - Look for this before concluding your dialect declares no data relations. It
188
+ is easy to miss, because it does not look like an edge — it looks like a
189
+ message in a request. This corpus asserted for a whole phase that
190
+ OpenInference declared nothing of the kind, while every multi-turn trace
191
+ carried it.
192
+ - Every declared edge needs a `basis` naming the source field — and, where the
193
+ library resolved a declaration from one granularity to another, saying so.
194
+
195
+ **Losslessness**
196
+ - `unmapped` — the attribute **keys** you saw and did not normalize. Keys only;
197
+ values are already in `raw` (`SPEC.md` §3.7).
198
+ - `raw` — the source record, verbatim and unmodified, plus its line number.
199
+
200
+ ## 4. Registering
201
+
202
+ Add to the registry in `spanweave/adapters/__init__.py`. Registration order must
203
+ not affect selection: ties are a hard error, not a first-wins race
204
+ (`SPEC.md` §6.1).
205
+
206
+ ## 5. Fixtures — not optional
207
+
208
+ An adapter without fixtures is not mergeable.
209
+
210
+ 1. Render **every** scenario in `fixtures/conformance/` in your dialect,
211
+ including all the degenerate ones (`FIXTURES.md` §3).
212
+
213
+ **Render from output you have observed, not from your reading of the
214
+ dialect's spec.** Run the instrumentor, look at what comes out, and write
215
+ that down. A rendering built from your understanding tests your
216
+ understanding: your adapter will agree with it, every test will pass, and
217
+ both will be wrong about the world in the same way. That is not
218
+ hypothetical — it is how all four call-bearing fixtures in this corpus came
219
+ to omit the conversation history that every real follow-up turn carries,
220
+ and how a pairing defect survived a full test suite until the first
221
+ captured trace (`FIXTURES.md` §5).
222
+ 2. Your renderings must produce the **existing** `expected/graph.json`,
223
+ unmodified. If they don't, the adapter is wrong — or the model is, and that
224
+ is a discussion, not an edit to the expectation (`FIXTURES.md` §4).
225
+ 3. Commit at least one **captured** trace from real instrumentation, with
226
+ provenance (`FIXTURES.md` §6). Hand-authored fixtures prove you matched our
227
+ understanding of the dialect; only a captured one proves you matched the
228
+ instrumentor.
229
+ 4. Add adapter-specific unit tests for quirks the shared corpus doesn't cover.
230
+ 5. **Every scenario must be either rendered or declared.** Since `TASKS.md`
231
+ 2.13 the corpus names both shipped dialects in
232
+ `tests/conformance.py:DIALECTS`, which turns on `FIXTURES.md` §4.3's
233
+ *silence is a failure* rule: a scenario your dialect cannot express needs an
234
+ `expected/coverage.json` entry with a reason, and a missing rendering that
235
+ nobody declared fails the build. There is **no third state and no exemption
236
+ list** — one existed between 2.7 and 2.13, held exactly one dialect, was
237
+ guarded and temporary, and was deleted rather than emptied. A new adapter
238
+ lands with its renderings, or it does not land.
239
+ 6. **Declaring is not free, and it expires.** A `coverage.json` reason is an
240
+ *invitation to check it against observed output*, not a settled fact — the
241
+ seed corpus's only user of the mechanism turned out to be wrong about its
242
+ own dialect (`FIXTURES.md` §4.3). Likewise a `comparison.json` declaration
243
+ that no dialect actually disagrees about fails the corpus, per field and per
244
+ entry. Both are recorded findings, not exemptions.
245
+
246
+ ### What the second dialect cost, as a worked expectation
247
+
248
+ `otel_genai` renders 17 of 21 scenarios. Four are declared, and the pattern is
249
+ worth knowing before you start: **three of the four are one missing kind.**
250
+ Nothing in the GenAI operation vocabulary maps to `NodeKind.chain`, so every
251
+ scenario pinning a `chain` node is out — including the only one carrying an
252
+ `EdgeKind.link`, which was not the coverage anybody expected to lose.
253
+
254
+ The lesson for a third adapter: coverage is lost to **kind vocabulary**, not to
255
+ attribute shape. Payload spellings differ everywhere and are handled by
256
+ declaration; a kind your dialect cannot name takes whole scenarios with it, and
257
+ often not the ones the scenario was written for.
258
+
259
+ **With one correction, from `TASKS.md` 2.17.** Payload spellings are handled by
260
+ declaration only while the dialects disagree about a payload's *value* or
261
+ *mime*. A dialect that emits **no attribute at all** where another emits one
262
+ produces `absent` against `present`, and `FIXTURES.md` §4.4 forbids declaring a
263
+ payload's `state` away — so that is coverage lost to attribute shape after all.
264
+ `retriever_and_embedding` is the case: OTel GenAI has no content attribute for
265
+ an embedding span, and no capture can retire that, because it is a property of
266
+ the convention rather than a gap in an adapter.
267
+
268
+ So the sharper version: coverage is lost to **what a dialect cannot say** —
269
+ usually a kind, sometimes an attribute that does not exist. Both are declared;
270
+ neither is a payload spelling.
271
+
272
+ ## 6. Checklist
273
+
274
+ - [ ] Single file under `spanweave/adapters/`; nothing else in the package touched.
275
+ - [ ] `detect()` pure, non-raising, keyed on distinctive markers, honestly scored.
276
+ - [ ] `parse()` pure, lazy, non-raising, order-independent.
277
+ - [ ] All five payload states distinguished; absent ≠ empty.
278
+ - [ ] No inferred pairings, no inferred data edges, no invented ids.
279
+ - [ ] `unmapped` keys recorded; `raw` preserved verbatim.
280
+ - [ ] Renderings derived from **observed instrumentor output**, not from a
281
+ reading of the dialect's spec.
282
+ - [ ] No `NodeKind` mapped from a convention value **no instrumentor can
283
+ emit** — map from what instrumentors produce, not from what the registry
284
+ defines (§3, `kind`).
285
+ - [ ] Requester ids taken only from what a span itself produced — history
286
+ echoes do not pair.
287
+ - [ ] All conformance scenarios rendered and passing against the **unmodified**
288
+ expected graphs, or declared in `coverage.json` with a reason checked
289
+ against observed output.
290
+ - [ ] Your dialect added to `tests/conformance.py:DIALECTS` in the same change
291
+ as its renderings — that line is what makes coverage un-rottable.
292
+ - [ ] One captured fixture with provenance.
293
+ - [ ] `make check` green — including the neutrality and layering gates.