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.
- spanweave-0.9.0/.github/workflows/ci.yml +64 -0
- spanweave-0.9.0/.gitignore +42 -0
- spanweave-0.9.0/ADAPTERS.md +293 -0
- spanweave-0.9.0/AGENT.md +391 -0
- spanweave-0.9.0/CLAUDE.md +115 -0
- spanweave-0.9.0/CONTRACTS.md +638 -0
- spanweave-0.9.0/CONTRIBUTING.md +92 -0
- spanweave-0.9.0/DESIGN.md +193 -0
- spanweave-0.9.0/ENVIRONMENT.md +139 -0
- spanweave-0.9.0/FIXTURES.md +567 -0
- spanweave-0.9.0/GLOSSARY.md +100 -0
- spanweave-0.9.0/LICENSE +21 -0
- spanweave-0.9.0/Makefile +104 -0
- spanweave-0.9.0/OPEN_QUESTIONS.md +407 -0
- spanweave-0.9.0/PKG-INFO +301 -0
- spanweave-0.9.0/PREDICTIONS.md +673 -0
- spanweave-0.9.0/README.md +275 -0
- spanweave-0.9.0/ROADMAP.md +458 -0
- spanweave-0.9.0/SECURITY.md +81 -0
- spanweave-0.9.0/SPEC.md +850 -0
- spanweave-0.9.0/TASKS.md +8192 -0
- spanweave-0.9.0/capture/README.md +490 -0
- spanweave-0.9.0/capture/__init__.py +12 -0
- spanweave-0.9.0/capture/backends.py +1385 -0
- spanweave-0.9.0/capture/exporter.py +153 -0
- spanweave-0.9.0/capture/fleet.py +389 -0
- spanweave-0.9.0/capture/run.py +626 -0
- spanweave-0.9.0/examples/__init__.py +1 -0
- spanweave-0.9.0/examples/cost_latency/__init__.py +981 -0
- spanweave-0.9.0/examples/cost_latency/__main__.py +352 -0
- spanweave-0.9.0/examples/cost_latency/load.py +105 -0
- spanweave-0.9.0/examples/fleet_aggregate/__init__.py +247 -0
- spanweave-0.9.0/examples/fleet_aggregate/__main__.py +139 -0
- spanweave-0.9.0/examples/trajectory_dump/__init__.py +588 -0
- spanweave-0.9.0/examples/trajectory_dump/__main__.py +211 -0
- spanweave-0.9.0/fixtures/captured/README.md +32 -0
- spanweave-0.9.0/fixtures/captured/genai_tool_call.jsonl +4 -0
- spanweave-0.9.0/fixtures/captured/genai_tool_call.provenance.md +87 -0
- spanweave-0.9.0/fixtures/captured/genai_workflow.jsonl +9 -0
- spanweave-0.9.0/fixtures/captured/genai_workflow.provenance.md +107 -0
- spanweave-0.9.0/fixtures/captured/openai_tool_call.jsonl +4 -0
- spanweave-0.9.0/fixtures/captured/openai_tool_call.provenance.md +94 -0
- spanweave-0.9.0/fixtures/conformance/README.md +70 -0
- spanweave-0.9.0/fixtures/conformance/clock_skew/dialects/openinference.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/clock_skew/dialects/otel_genai.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/clock_skew/expected/comparison.json +6 -0
- spanweave-0.9.0/fixtures/conformance/clock_skew/expected/diagnostics.json +13 -0
- spanweave-0.9.0/fixtures/conformance/clock_skew/expected/graph.json +100 -0
- spanweave-0.9.0/fixtures/conformance/clock_skew/otel_genai.notes.md +19 -0
- spanweave-0.9.0/fixtures/conformance/clock_skew/scenario.md +54 -0
- spanweave-0.9.0/fixtures/conformance/cyclic_parents/dialects/openinference.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/cyclic_parents/expected/coverage.json +7 -0
- spanweave-0.9.0/fixtures/conformance/cyclic_parents/expected/diagnostics.json +9 -0
- spanweave-0.9.0/fixtures/conformance/cyclic_parents/expected/graph.json +77 -0
- spanweave-0.9.0/fixtures/conformance/cyclic_parents/scenario.md +62 -0
- spanweave-0.9.0/fixtures/conformance/declared_data_edge/dialects/openinference.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/declared_data_edge/dialects/otel_genai.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/declared_data_edge/expected/comparison.json +17 -0
- spanweave-0.9.0/fixtures/conformance/declared_data_edge/expected/diagnostics.json +13 -0
- spanweave-0.9.0/fixtures/conformance/declared_data_edge/expected/graph.json +117 -0
- spanweave-0.9.0/fixtures/conformance/declared_data_edge/expected/payloads/otel_genai.json +53 -0
- spanweave-0.9.0/fixtures/conformance/declared_data_edge/otel_genai.notes.md +32 -0
- spanweave-0.9.0/fixtures/conformance/declared_data_edge/scenario.md +84 -0
- spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/dialects/openinference.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/dialects/otel_genai.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/expected/error.json +5 -0
- spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/otel_genai.notes.md +13 -0
- spanweave-0.9.0/fixtures/conformance/duplicate_span_ids/scenario.md +41 -0
- spanweave-0.9.0/fixtures/conformance/empty_payload/dialects/openinference.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/empty_payload/dialects/otel_genai.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/empty_payload/expected/comparison.json +15 -0
- spanweave-0.9.0/fixtures/conformance/empty_payload/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/empty_payload/expected/graph.json +34 -0
- spanweave-0.9.0/fixtures/conformance/empty_payload/expected/payloads/otel_genai.json +7 -0
- spanweave-0.9.0/fixtures/conformance/empty_payload/otel_genai.notes.md +37 -0
- spanweave-0.9.0/fixtures/conformance/empty_payload/scenario.md +43 -0
- spanweave-0.9.0/fixtures/conformance/llm_tool_llm/dialects/openinference.jsonl +4 -0
- spanweave-0.9.0/fixtures/conformance/llm_tool_llm/dialects/otel_genai.jsonl +4 -0
- spanweave-0.9.0/fixtures/conformance/llm_tool_llm/expected/comparison.json +27 -0
- spanweave-0.9.0/fixtures/conformance/llm_tool_llm/expected/diagnostics.json +9 -0
- spanweave-0.9.0/fixtures/conformance/llm_tool_llm/expected/graph.json +229 -0
- spanweave-0.9.0/fixtures/conformance/llm_tool_llm/expected/payloads/otel_genai.json +99 -0
- spanweave-0.9.0/fixtures/conformance/llm_tool_llm/otel_genai.notes.md +110 -0
- spanweave-0.9.0/fixtures/conformance/llm_tool_llm/scenario.md +134 -0
- spanweave-0.9.0/fixtures/conformance/malformed_payload_json/dialects/openinference.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/malformed_payload_json/dialects/otel_genai.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/malformed_payload_json/expected/comparison.json +6 -0
- spanweave-0.9.0/fixtures/conformance/malformed_payload_json/expected/diagnostics.json +9 -0
- spanweave-0.9.0/fixtures/conformance/malformed_payload_json/expected/graph.json +39 -0
- spanweave-0.9.0/fixtures/conformance/malformed_payload_json/otel_genai.notes.md +26 -0
- spanweave-0.9.0/fixtures/conformance/malformed_payload_json/scenario.md +52 -0
- spanweave-0.9.0/fixtures/conformance/missing_payloads/dialects/openinference.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/missing_payloads/dialects/otel_genai.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/missing_payloads/expected/comparison.json +6 -0
- spanweave-0.9.0/fixtures/conformance/missing_payloads/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/missing_payloads/expected/graph.json +63 -0
- spanweave-0.9.0/fixtures/conformance/missing_payloads/otel_genai.notes.md +30 -0
- spanweave-0.9.0/fixtures/conformance/missing_payloads/scenario.md +38 -0
- spanweave-0.9.0/fixtures/conformance/nested_agents/dialects/openinference.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/nested_agents/dialects/otel_genai.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/nested_agents/expected/comparison.json +6 -0
- spanweave-0.9.0/fixtures/conformance/nested_agents/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/nested_agents/expected/graph.json +91 -0
- spanweave-0.9.0/fixtures/conformance/nested_agents/otel_genai.notes.md +22 -0
- spanweave-0.9.0/fixtures/conformance/nested_agents/scenario.md +42 -0
- spanweave-0.9.0/fixtures/conformance/orphan_parent/dialects/openinference.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/orphan_parent/dialects/otel_genai.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/orphan_parent/expected/comparison.json +6 -0
- spanweave-0.9.0/fixtures/conformance/orphan_parent/expected/diagnostics.json +9 -0
- spanweave-0.9.0/fixtures/conformance/orphan_parent/expected/graph.json +39 -0
- spanweave-0.9.0/fixtures/conformance/orphan_parent/otel_genai.notes.md +12 -0
- spanweave-0.9.0/fixtures/conformance/orphan_parent/scenario.md +43 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/dialects/openinference.jsonl +4 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/dialects/otel_genai.jsonl +4 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/expected/comparison.json +21 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/expected/diagnostics.json +9 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/expected/graph.json +184 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/expected/payloads/otel_genai.json +56 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/otel_genai.notes.md +71 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tool_calls/scenario.md +84 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tools/dialects/openinference.jsonl +4 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tools/dialects/otel_genai.jsonl +4 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tools/expected/comparison.json +15 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tools/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tools/expected/graph.json +133 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tools/expected/payloads/otel_genai.json +17 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tools/otel_genai.notes.md +25 -0
- spanweave-0.9.0/fixtures/conformance/parallel_tools/scenario.md +64 -0
- spanweave-0.9.0/fixtures/conformance/redacted_payload/dialects/openinference.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/redacted_payload/expected/coverage.json +7 -0
- spanweave-0.9.0/fixtures/conformance/redacted_payload/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/redacted_payload/expected/graph.json +37 -0
- spanweave-0.9.0/fixtures/conformance/redacted_payload/scenario.md +68 -0
- spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/dialects/openinference.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/expected/coverage.json +7 -0
- spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/expected/graph.json +110 -0
- spanweave-0.9.0/fixtures/conformance/retriever_and_embedding/scenario.md +109 -0
- spanweave-0.9.0/fixtures/conformance/shuffled_order/dialects/openinference.jsonl +4 -0
- spanweave-0.9.0/fixtures/conformance/shuffled_order/dialects/otel_genai.jsonl +4 -0
- spanweave-0.9.0/fixtures/conformance/shuffled_order/expected/comparison.json +27 -0
- spanweave-0.9.0/fixtures/conformance/shuffled_order/expected/diagnostics.json +9 -0
- spanweave-0.9.0/fixtures/conformance/shuffled_order/expected/graph.json +229 -0
- spanweave-0.9.0/fixtures/conformance/shuffled_order/expected/payloads/otel_genai.json +99 -0
- spanweave-0.9.0/fixtures/conformance/shuffled_order/otel_genai.notes.md +14 -0
- spanweave-0.9.0/fixtures/conformance/shuffled_order/scenario.md +45 -0
- spanweave-0.9.0/fixtures/conformance/single_tool_call/dialects/openinference.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/single_tool_call/dialects/otel_genai.jsonl +1 -0
- spanweave-0.9.0/fixtures/conformance/single_tool_call/expected/comparison.json +6 -0
- spanweave-0.9.0/fixtures/conformance/single_tool_call/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/single_tool_call/expected/graph.json +38 -0
- spanweave-0.9.0/fixtures/conformance/single_tool_call/otel_genai.notes.md +19 -0
- spanweave-0.9.0/fixtures/conformance/single_tool_call/scenario.md +41 -0
- spanweave-0.9.0/fixtures/conformance/span_links/dialects/openinference.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/span_links/expected/coverage.json +7 -0
- spanweave-0.9.0/fixtures/conformance/span_links/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/span_links/expected/graph.json +79 -0
- spanweave-0.9.0/fixtures/conformance/span_links/scenario.md +77 -0
- spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/dialects/openinference.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/dialects/otel_genai.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/expected/comparison.json +23 -0
- spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/expected/diagnostics.json +9 -0
- spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/expected/graph.json +174 -0
- spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/expected/payloads/otel_genai.json +85 -0
- spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/otel_genai.notes.md +89 -0
- spanweave-0.9.0/fixtures/conformance/tool_call_history_echo/scenario.md +103 -0
- spanweave-0.9.0/fixtures/conformance/unknown_kind/dialects/openinference.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/unknown_kind/dialects/otel_genai.jsonl +2 -0
- spanweave-0.9.0/fixtures/conformance/unknown_kind/expected/comparison.json +7 -0
- spanweave-0.9.0/fixtures/conformance/unknown_kind/expected/diagnostics.json +9 -0
- spanweave-0.9.0/fixtures/conformance/unknown_kind/expected/graph.json +69 -0
- spanweave-0.9.0/fixtures/conformance/unknown_kind/otel_genai.notes.md +44 -0
- spanweave-0.9.0/fixtures/conformance/unknown_kind/scenario.md +90 -0
- spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/dialects/openinference.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/dialects/otel_genai.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/expected/comparison.json +17 -0
- spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/expected/diagnostics.json +17 -0
- spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/expected/graph.json +137 -0
- spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/expected/payloads/otel_genai.json +34 -0
- spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/otel_genai.notes.md +134 -0
- spanweave-0.9.0/fixtures/conformance/unpaired_tool_call/scenario.md +66 -0
- spanweave-0.9.0/fixtures/conformance/unset_and_error_status/dialects/openinference.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/unset_and_error_status/dialects/otel_genai.jsonl +3 -0
- spanweave-0.9.0/fixtures/conformance/unset_and_error_status/expected/comparison.json +6 -0
- spanweave-0.9.0/fixtures/conformance/unset_and_error_status/expected/diagnostics.json +4 -0
- spanweave-0.9.0/fixtures/conformance/unset_and_error_status/expected/graph.json +100 -0
- spanweave-0.9.0/fixtures/conformance/unset_and_error_status/provenance.notes.md +56 -0
- spanweave-0.9.0/fixtures/conformance/unset_and_error_status/scenario.md +71 -0
- spanweave-0.9.0/pyproject.toml +107 -0
- spanweave-0.9.0/spanweave/__init__.py +77 -0
- spanweave-0.9.0/spanweave/adapters/__init__.py +161 -0
- spanweave-0.9.0/spanweave/adapters/base.py +58 -0
- spanweave-0.9.0/spanweave/adapters/openinference.py +544 -0
- spanweave-0.9.0/spanweave/adapters/otel_genai.py +709 -0
- spanweave-0.9.0/spanweave/annotate.py +132 -0
- spanweave-0.9.0/spanweave/api.py +43 -0
- spanweave-0.9.0/spanweave/build.py +595 -0
- spanweave-0.9.0/spanweave/cli.py +264 -0
- spanweave-0.9.0/spanweave/diagnostics.py +116 -0
- spanweave-0.9.0/spanweave/errors.py +97 -0
- spanweave-0.9.0/spanweave/graph.py +321 -0
- spanweave-0.9.0/spanweave/ids.py +97 -0
- spanweave-0.9.0/spanweave/model.py +319 -0
- spanweave-0.9.0/spanweave/read.py +185 -0
- spanweave-0.9.0/spanweave/seam.py +142 -0
- spanweave-0.9.0/spanweave/serialize.py +293 -0
- spanweave-0.9.0/spanweave/version.py +19 -0
- spanweave-0.9.0/tests/__init__.py +0 -0
- spanweave-0.9.0/tests/conformance.py +357 -0
- spanweave-0.9.0/tests/conftest.py +73 -0
- spanweave-0.9.0/tests/determinism.py +128 -0
- spanweave-0.9.0/tests/gates.py +403 -0
- spanweave-0.9.0/tests/install_check.py +928 -0
- spanweave-0.9.0/tests/readme_quickstart.py +119 -0
- spanweave-0.9.0/tests/schema_shape.py +389 -0
- spanweave-0.9.0/tests/serialized_shape.json +225 -0
- spanweave-0.9.0/tests/stranger_path.py +212 -0
- spanweave-0.9.0/tests/test_acceptance.py +202 -0
- spanweave-0.9.0/tests/test_adapters.py +223 -0
- spanweave-0.9.0/tests/test_build.py +522 -0
- spanweave-0.9.0/tests/test_capture.py +2026 -0
- spanweave-0.9.0/tests/test_cli.py +157 -0
- spanweave-0.9.0/tests/test_codes.py +286 -0
- spanweave-0.9.0/tests/test_conformance.py +946 -0
- spanweave-0.9.0/tests/test_contracts.py +459 -0
- spanweave-0.9.0/tests/test_detection.py +204 -0
- spanweave-0.9.0/tests/test_determinism.py +242 -0
- spanweave-0.9.0/tests/test_doc_truth.py +669 -0
- spanweave-0.9.0/tests/test_docs.py +107 -0
- spanweave-0.9.0/tests/test_example_cost_latency.py +647 -0
- spanweave-0.9.0/tests/test_example_fleet_aggregate.py +241 -0
- spanweave-0.9.0/tests/test_example_trajectory_dump.py +505 -0
- spanweave-0.9.0/tests/test_gates.py +228 -0
- spanweave-0.9.0/tests/test_graph.py +380 -0
- spanweave-0.9.0/tests/test_ids.py +134 -0
- spanweave-0.9.0/tests/test_model.py +285 -0
- spanweave-0.9.0/tests/test_openinference.py +611 -0
- spanweave-0.9.0/tests/test_ordering.py +291 -0
- spanweave-0.9.0/tests/test_otel_genai.py +659 -0
- spanweave-0.9.0/tests/test_prediction_evidence.py +331 -0
- spanweave-0.9.0/tests/test_read.py +145 -0
- spanweave-0.9.0/tests/test_readme_quickstart.py +195 -0
- spanweave-0.9.0/tests/test_schema_shape.py +246 -0
- spanweave-0.9.0/tests/test_serialize.py +173 -0
- spanweave-0.9.0/tests/test_version.py +71 -0
- 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.
|