agent-tracegraph 0.2.0__tar.gz → 0.3.1__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 (36) hide show
  1. agent_tracegraph-0.3.1/CHANGELOG.md +329 -0
  2. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/PKG-INFO +52 -18
  3. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/README.md +50 -16
  4. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/contracts/phoenix-runtime.json +1 -1
  5. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/pyproject.toml +14 -2
  6. agent_tracegraph-0.3.1/src/tracegraph/_atomic_write.py +41 -0
  7. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/adapters/langgraph_checkpoint.py +150 -44
  8. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/adapters/otlp_spans.py +189 -68
  9. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/adapters/phoenix_export.py +51 -43
  10. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/analysis/ahu.py +13 -11
  11. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/analysis/diagnose.py +118 -94
  12. agent_tracegraph-0.3.1/src/tracegraph/analysis/memo.py +193 -0
  13. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/analysis/patterns.py +15 -6
  14. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/artifact.py +45 -37
  15. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/cli.py +98 -90
  16. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/model.py +13 -2
  17. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/normalize.py +134 -40
  18. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/review_candidates.py +6 -19
  19. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/store/in_memory.py +22 -0
  20. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/store/ladybug.py +16 -3
  21. agent_tracegraph-0.2.0/CHANGELOG.md +0 -117
  22. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/.gitignore +0 -0
  23. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/LICENSE +0 -0
  24. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/SECURITY.md +0 -0
  25. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/contracts/analysis-report.schema.json +0 -0
  26. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/contracts/review-candidates.schema.json +0 -0
  27. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/__init__.py +0 -0
  28. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/adapters/__init__.py +0 -0
  29. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/adapters/base.py +0 -0
  30. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/analysis/__init__.py +0 -0
  31. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/analysis/explain.py +0 -0
  32. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/input_validation.py +0 -0
  33. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/py.typed +0 -0
  34. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/sqlite_snapshot.py +0 -0
  35. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/store/__init__.py +0 -0
  36. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.1}/src/tracegraph/store/base.py +0 -0
@@ -0,0 +1,329 @@
1
+ # Changelog
2
+
3
+ All notable changes to Tracegraph are documented here.
4
+
5
+ The **artifact** schema version and the **report**/**review-candidate** JSON schemas are
6
+ separate contracts from this version number; changes to any of them are called out
7
+ explicitly.
8
+
9
+ ## 0.3.1 - 2026-10-13
10
+
11
+ ### Fixed
12
+
13
+ - **An unreadable cost no longer understates the run's total.** The importers dropped a
14
+ `llm.cost.total` that was not a finite non-negative decimal (`"abc"`, `-1`, a boolean), so
15
+ `analyze` summed the remaining costs and printed a total that was knowably too low, with no
16
+ warning. The documented rule that a single unparseable cost withholds the whole total only
17
+ applied to artifacts built by hand. An unreadable total cost is now stored as the fixed
18
+ marker `"unreadable"`, never the producer's value, and the report withholds `total_cost`
19
+ and its delta with the existing note.
20
+ - Token counts, prompt and completion costs, cost currencies and Phoenix evaluation scores
21
+ that cannot be read were dropped without a trace. They are still reported as unavailable,
22
+ and `ingest-otlp`, `ingest-phoenix`, `analyze` and `phoenix diagnose` now say so on stderr,
23
+ naming the kind of value and how many spans (for scores, evaluations) carried it, never
24
+ the value itself. A boolean score still reads as `1.0` or `0.0`, as before.
25
+ - An OTLP `intValue` sent as a fractional JSON number was truncated before validation:
26
+ `1.5` became `1` and `-0.5` became `0`, so an invalid token count looked valid. It is now
27
+ validated as sent.
28
+ - **A checkpoint history LangGraph could not have written is a usage error, not a
29
+ traceback or a different trace.** About half of 64 one-field mutations of a real history
30
+ (a non-object `metadata` or `channel_values`, a numeric `checkpoint_ns`, a boolean `v`, an
31
+ unhashable parent checkpoint id, ...) crashed `ingest` with a traceback, and several were
32
+ read silently as something else: a malformed parent config or `metadata.parents` dropped
33
+ the declared parent and made the checkpoint a root, and `metadata.step` of `true` or `1.5`
34
+ was read as step 1. Each checkpoint is now checked against the types LangGraph declares
35
+ for the fields tracegraph reads, strictly, and rejected with a message naming the field.
36
+ A config `checkpoint_id` that disagrees with the checkpoint's own id is rejected too.
37
+ - A checkpoint row the saver cannot decode (an unknown serialization type, metadata nested
38
+ past the recursion limit, a non-string metadata column) crashed `ingest` and `discover`
39
+ with a traceback, except that `discover` reported an unknown serialization type as "does
40
+ not support listing all threads". Both now report the history as undecodable.
41
+ - Pending writes that are not a `(task id, channel, value)` triple were skipped without a
42
+ word, and any 3-character string was unpacked as one (`"abc"` became task `a`). They are
43
+ still ignored, but `ingest` now says how many on stderr, since one could have carried a
44
+ task failure.
45
+
46
+ ### Compatibility notes for imports
47
+
48
+ - An artifact imported from a span with an unreadable total cost now carries
49
+ `"total_cost": "unreadable"` instead of no cost, and its reports withhold the run's
50
+ total cost. No schema change: 0.3.0 reads such an artifact and withholds the same way.
51
+ - Because `intValue` is no longer truncated, `llm.cost.total = {"intValue": 1.5}` is stored as
52
+ `"1.5"` instead of `"1"`, and a `toolgraph.graph_generation` of `{"intValue": 1.5}` is
53
+ rejected like any other invalid generation instead of being read as `1`.
54
+ - The committed Phoenix fixture carries an unreadable `llm.cost.prompt`; reading it now prints
55
+ one stderr disclosure. Every committed fixture's artifact, report, memo, candidate file and
56
+ CLI stdout is unchanged.
57
+ - LangGraph histories that `ingest` used to accept with a malformed field listed above are
58
+ now refused. Histories written by LangGraph's own savers are unaffected: the checks cover
59
+ only fields tracegraph reads (`counters_since_delta_snapshot`, which savers return as
60
+ lists although it is declared as tuples, is not checked), a missing `updated_channels`
61
+ and a legacy `pending_sends` are accepted, and an unknown `metadata.source` string still
62
+ reads as a loop. The committed LangGraph fixture is byte-identical.
63
+
64
+ ## 0.3.0 - 2026-10-13
65
+
66
+ ### Fixed
67
+
68
+ - An artifact whose content was not canonical (for example a stored trace status that
69
+ disagreed with its steps) was stamped `schema_version` 4 even when it carried no edge
70
+ origins. Version 4 is now stamped only when the file's TREE_PARENT edges are the
71
+ origin-priority projection of its causal edges and that differs from the temporal one; the
72
+ rest of the trace does not affect the stamp. **Digest impact:** canonical artifacts are
73
+ unchanged, but re-serializing a non-canonical trace (one `validate` rejects) whose tree is
74
+ not the origin-priority one, such as a trace with its derived edges missing, now writes
75
+ version 2 or 3 instead of 4. Any digest computed from that re-serialization changes,
76
+ including the `artifact_digest` that `analyze` reports for such a trace.
77
+ - Loading now rejects an envelope that understates its content: a `schema_version` 2 file
78
+ holding an origin-priority projection or derived task steps. Every reader that trusts the
79
+ envelope would have read such a file under the wrong rules, and a re-save rewrote it as a
80
+ different file. An envelope may still overstate what its content needs.
81
+ - `schema_version` must be an integer. `true`, `1.0`, `2.0` and `4.0` were accepted because
82
+ they compare equal, and `true` was migrated as a version 1 file.
83
+ - Evidence floats (`duration_ms`, evaluation `score`) must be finite, and the writer refuses
84
+ to emit `NaN`/`Infinity`, which are not JSON.
85
+ - `--backend ladybug` crashed with a raw traceback on a step `seq` outside the INT64 range
86
+ the backend stores. It now exits with a usage error naming the limit.
87
+ - `InMemoryStore.upsert_edges` accepted a `CAUSED_BY`/`TREE_PARENT` edge whose endpoint was
88
+ not a stored step, or a `BELONGS_TO` edge pointing at another trace; the failure surfaced
89
+ later as a `KeyError` from `ancestors`. It now raises `ValueError` and inserts nothing from
90
+ that batch, matching the LadybugDB store. `WROTE`/`READ` channel edges are unchecked as
91
+ before, and node upsert still replaces by ID.
92
+ - OTLP and Phoenix imports crashed with a raw traceback on a timestamp of `Infinity`, `NaN`
93
+ or a number beyond the representable date range, on a `spanId`/`parentSpanId` given as a
94
+ list, and on Phoenix `annotations` that were not an array. A timestamp that cannot be read
95
+ — including garbage text, which was silently dropped before — is now reported as
96
+ unavailable and disclosed on stderr by `ingest-otlp`, `ingest-phoenix`, `analyze` and
97
+ `phoenix diagnose`. Malformed ids and annotations are rejected with a usage error.
98
+ - An OTLP `status.code` of `true` was read as OK because `true == 1`, and `false` as UNSET
99
+ because `false == 0`; both are now rejected, for OTLP and Phoenix imports alike.
100
+ - LangGraph: two derived task steps could receive the same id (checkpoint `A` with task
101
+ `x#task:y`, checkpoint `A#task:x` with task `y`); the second silently replaced the first
102
+ and gained a second cause it never had. The collision is now rejected like a collision
103
+ with a checkpoint.
104
+ - LangGraph: a failed task that no candidate node reproduces is still reported unnamed, and
105
+ the read now says so on stderr. An unnamed failure is also what a change in LangGraph's
106
+ task-id algorithm would look like, so it is no longer silent.
107
+
108
+ - `diff` and `validate` printed producer text through Rich markup: a step named
109
+ `tool[bold]` showed as `tool`, and an invalid edge naming `ghost[red]X[/red]` showed as
110
+ `ghostX`. Both now print the text literally, as `inspect`, `explain` and `query` already
111
+ did. Output paths are printed literally too: an `--out` path containing `[/]` used to
112
+ raise a markup error after the file had been written, so the command exited 1 for work
113
+ it had completed.
114
+ - `diff --structure` paired any two leftover subtrees because every label is empty under
115
+ that option, then reported them under an empty path (`only in B under > : subtree ·`).
116
+ An empty label is no longer treated as evidence of correspondence, and the reported
117
+ subtrees now show step names so they can be found, while the comparison still ignores
118
+ them. The identical/not-identical verdict and exit code are unchanged.
119
+ - `explain` and `query --explain` labeled a nameless step by its `source`, which for
120
+ OTLP and Phoenix spans is always `loop` and for LangGraph is checkpoint metadata
121
+ (`input`, `loop`), not what the step was. They now use the step kind, like every other
122
+ view.
123
+ - `export-incident-memo` wrote `links_preserved` as `true`/`false`/`null` in its front
124
+ matter but as `True`/`False`/`None` in its summary. Both now use the front-matter form.
125
+ - The Phoenix CLI version-floor message is derived from the floor constant instead of
126
+ repeating it.
127
+
128
+ - `analyze --baseline` published token deltas, and cost deltas, from totals that covered
129
+ only some steps on one side: with tokens on 3 of 3 steps in the baseline and 1 of 3 in
130
+ the current run it reported `-200`, although the silent steps could have used any amount.
131
+ Like the duration delta, these deltas are now withheld with a note unless every step on
132
+ both sides reports the metric. In traces where only model calls carry token counts this
133
+ withholds the token delta. A cost total or delta withheld because steps or runs report
134
+ different or unknown currencies is now disclosed too; before, `analyze` printed
135
+ `cost=unavailable` with no reason.
136
+
137
+ - Writing an artifact or an analysis report left a hidden temporary file behind when the
138
+ write or fsync failed, because the temporary name was recorded only after writing. All
139
+ four output writers now share one helper that removes it on every failure.
140
+
141
+ ### Compatibility notes for imports
142
+
143
+ These change what an input that imported successfully before now produces or whether it
144
+ imports at all. No committed fixture is affected.
145
+
146
+ - An OTLP token count given as a boolean (`true`, or `{"intValue": true}`) was stored as `1`;
147
+ it is now unavailable, matching how an unusable cost is already treated. A non-finite
148
+ token count is unavailable too instead of crashing the import. An `intValue` holding a
149
+ boolean is no longer converted to `0`/`1` for any attribute.
150
+ - Raw OTLP input could inject evaluation summaries through the private
151
+ `_tracegraph_evaluations` span key, bypassing the length limit the Phoenix importer
152
+ applies. Only the Phoenix importer reads that key now.
153
+ - An empty-string `traceId` or `spanId` is rejected. A `parentSpanId` of `0`, `false` or
154
+ `[]` used to be read as "no parent", which made a span with a malformed declared parent a
155
+ root; it is now rejected. A missing, `null` or empty-string `parentSpanId` still marks a
156
+ root span.
157
+ - A Phoenix evaluation score too large to represent crashed the import; it is now
158
+ unavailable, like a non-finite score.
159
+ - When several spans carry the `graph.node.parent_id` a span declares (a loop), the importer
160
+ picks the nearest execution that started earlier. A span with a missing or unreadable
161
+ start time sorted as time 0 and could win that choice, so a timestamp nobody recorded
162
+ decided a causal edge. Such spans no longer take part: the choice is treated as
163
+ ambiguous, and the span falls back to its declared `parentSpanId`, or the import is
164
+ rejected if it has none.
165
+ - The CLA workflow refused every pull request from the maintainer's own commit-author
166
+ account, which is not the account that owns the repository. Both are allowlisted now.
167
+ - The release documentation asked for a personal access token with *read* access to pull
168
+ requests. Posting the CLA comment failed with that configuration and succeeded after
169
+ raising it to write; the cause remains unconfirmed, since GitHub documents either Issues
170
+ write or Pull requests write as sufficient. The documentation now asks for write on
171
+ Contents, Issues and Pull requests alike and records the observation rather than
172
+ inferring a rule.
173
+ - It also did not say that a Trusted Publisher's "Environment name" is the GitHub
174
+ environment rather than the index, which failed the first 0.2.0 rehearsal.
175
+
176
+ ### Dependencies
177
+
178
+ - The optional `[cypher]` extra now accepts LadybugDB `>=0.18,<0.22` (was `<0.19`), and the
179
+ lockfile moves to 0.21.2. The full test suite and every fixture's output are unchanged
180
+ on 0.21.2.
181
+ - CI now tests against Phoenix CLI 1.18.8 (was 1.8.1). The runtime floor stays 1.0.4: this
182
+ pin is what CI proves, not what tracegraph requires.
183
+
184
+ ### Added
185
+
186
+ - `tracegraph export-incident-memo <artifact> -o memo.md` writes a Markdown post-mortem
187
+ intended for long-term memory stores. Its output is a strict allowlist: the run id appears
188
+ only as a truncated SHA-256 `run_digest`, steps appear as `step_<n>_<kind>` aliases, and
189
+ no step name, error message, evaluation label or payload is written. Edges that only show
190
+ containment or have no recorded origin are rendered as "preceded by", never "caused by".
191
+ The memo carries the SHA-256 of the exact artifact bytes it was built from.
192
+
193
+ ### Changed
194
+
195
+ - **Artifact schema version 4.** When a step has several causes, its single TREE_PARENT is
196
+ now chosen by edge origin first — a declared graph or checkpoint parent over span
197
+ containment, over a span link, over an unknown origin — and only then by time. An artifact
198
+ whose tree differs from the purely temporal choice is stamped version 4; every other
199
+ artifact serializes as version 2 or 3, byte for byte as before. **Readers from 0.2.0 and
200
+ older refuse version 4 files** with an explicit unsupported-version error. This build reads
201
+ both projections, and the LadybugDB store re-emits a legacy file unchanged. The derived
202
+ tree feeds `inspect`, `diff`, and `analyze --baseline`, where it decides both the topology
203
+ changes and how steps are matched between the two runs: a step whose tree parent moved
204
+ can now appear as one removed and one added behavior change, and its logical step key
205
+ changes. Raw causal queries (`explain`, `query`) are unaffected.
206
+ - Phoenix timestamps without a timezone are now read as UTC rather than in the local
207
+ timezone of the machine running the import. **Digest impact:** artifacts built from such
208
+ exports now hash the same on every machine, and differ from any artifact previously built
209
+ where the local timezone applied a nonzero offset to those timestamps. Timestamps that
210
+ carry a `Z` or an offset are unchanged.
211
+ - **Phoenix artifacts no longer carry free-text annotation names or labels.** The Phoenix
212
+ import is documented as body-free and already kept only identifier-shaped span names, but
213
+ annotation names and labels were kept as free text (whitespace-collapsed, cut to 200
214
+ characters), so an evaluator's note such as `user said: my password is hunter2` reached
215
+ the artifact. They now pass the same identifier filter as span names: an evaluation whose
216
+ name fails it is dropped, and a label that fails it becomes `null` while the score is
217
+ kept. **Artifact content change:** a re-import of a Phoenix export with such annotations
218
+ produces different bytes and digests. Identifier-shaped text, which can still be personal
219
+ data such as an e-mail address, is kept.
220
+
221
+ ## 0.2.0 - 2026-09-12
222
+
223
+ First published release. `0.1.0` was tagged but never uploaded.
224
+
225
+ ### Fixed
226
+
227
+ - **A LangGraph node that raised an exception was reported as a successful run**
228
+ (`status=ok`, `error_count=0`). LangGraph does not route such a failure through a state
229
+ channel: it records it in the checkpoint's pending writes and writes no further checkpoint,
230
+ so reading channel values alone could not see it and no `--error-channel` value recovered
231
+ it. Each failed task now becomes its own step, hanging off the checkpoint that scheduled it
232
+ rather than marking that checkpoint — which is named after the node that *produced* it, so
233
+ marking it reported the wrong node.
234
+ - A task cancelled because a *sibling* failed is no longer counted as a second failure.
235
+ LangGraph records sibling cancellation through the same `__error__` channel as a genuine
236
+ fault; those tasks are reported as `unset` with no message, so one fault stays one failure
237
+ and a healthy node is never a primary candidate.
238
+
239
+ ### Security
240
+
241
+ - **Reading a checkpoint database no longer executes what it contains.** LangGraph's
242
+ deserializer revives stored objects by importing a module and calling a name, both taken
243
+ from the payload, and by default an unrecognized target is logged and then called — so a
244
+ crafted checkpoint naming `os.system` or `subprocess.run` ran on the machine doing the
245
+ analysis (both verified against the pinned dependency). `tracegraph ingest` now passes a
246
+ serializer with an empty allowlist.
247
+
248
+ **Behaviour change:** objects outside the framework's registered types no longer revive
249
+ through `ingest`; they come back as their raw arguments. If you were relying on a custom
250
+ class being reconstructed from checkpoint state, it will now surface as plain data.
251
+ Constructing `LangGraphCheckpointAdapter` yourself is unaffected — the serializer is
252
+ whichever one your saver carries.
253
+
254
+ ### Added
255
+
256
+ - The failing node's name, recovered by recomputing LangGraph's task id from checkpoint data.
257
+ The match is self-verifying, so a task nothing identifies keeps no name rather than
258
+ borrowing a neighbour's.
259
+ - `StepSource.TASK`, marking a step derived from pending writes rather than from a checkpoint.
260
+ - `RawTrace.ingest_warnings`, printed by `ingest` to stderr — disclosures about the *read*
261
+ (for example `Send` packets that could not be decoded), which are not part of the run and so
262
+ never enter the artifact.
263
+ - First public release metadata: Apache-2.0 `LICENSE`, `CLA.md`, `CODE_OF_CONDUCT.md`,
264
+ `CONTRIBUTING.md`, `SECURITY.md`, and PyPI classifiers, keywords, authors and project URLs.
265
+ - `tracegraph --version` (`-V`), which prints the bare version and nothing else. There was no
266
+ way to ask an installed build what it was.
267
+ - An sdist allowlist. Previously the source distribution shipped everything git tracked —
268
+ tests, docs, CI workflows, the lockfile and the checkpoint fixtures.
269
+ - A tag-triggered release workflow publishing through PyPI Trusted Publishing: `test-v*`
270
+ rehearses against TestPyPI, `v*` publishes to PyPI. Only the publishing job holds an OIDC
271
+ token, and it checks out nothing, installs nothing, and uploads only files whose digests
272
+ match what the build job recorded. Before any of that, the build refuses a tag whose
273
+ commit is not on the default branch, has no green `tests` run of its own, disagrees with
274
+ the packaged version, or has no dated changelog entry.
275
+ - `scripts/verify_artifacts.py`, which checks that the built distributions are exactly the
276
+ two files that will be uploaded and that the source distribution carries only what the
277
+ packaging allowlist intends. It also refuses tests, docs, CI configuration and the
278
+ lockfile outright, so widening that allowlist cannot quietly start shipping them, and
279
+ refuses any archive member whose path does not go where it reads — a `..` segment is
280
+ what would let a member satisfy both lists and still land somewhere else, or outside
281
+ the archive entirely, when someone extracts it.
282
+ - `scripts/require_ci_success.py`, the release gate described above.
283
+ - A [release runbook](docs/releasing.md) and a
284
+ [public release checklist](docs/public-release-checklist.md).
285
+ - The installed-wheel CI jobs now pin the version they expect and run the artifact
286
+ verifier, so the release checks are exercised on every pull request rather than first at
287
+ tag time.
288
+
289
+ ### Changed
290
+
291
+ - **The distribution is now `agent-tracegraph`.** The plain `tracegraph` name on PyPI belongs
292
+ to an unrelated project. The import package, the console script, the schema `kind`
293
+ constants and the `TRACEGRAPH_*` environment variables are **unchanged** — only the name
294
+ you install differs. Installing from source or from a checkout is unaffected.
295
+ - The build backend version is pinned (`hatchling==1.32.0`), so a release rehearsal and the
296
+ production tag cannot run *different backend versions* against the same commit. This is
297
+ drift control, not reproducibility: hatchling's own dependencies still float. The uv
298
+ version used to build is pinned in the same way.
299
+ - `twine check` is a gate again, pinned to `twine@7.0.0`. It was disabled because twine 6
300
+ rejected the `Metadata-Version: 2.5` this backend emits; twine 7 accepts it.
301
+ - **Artifact schema version 3**, stamped *by content*: an artifact carrying derived task steps
302
+ declares 3 so an older reader refuses it by name instead of failing on an enum, while
303
+ artifacts without them still serialize as version 2 byte for byte. The envelope is inside
304
+ the digest `export-review-candidates` binds to, so a blanket bump would have invalidated
305
+ digests already published for unchanged artifacts.
306
+ - The LangGraph capture disclosure in every report now names both the error channel and task
307
+ pending writes. It remains unconditional: an artifact ingested by an older build, or one
308
+ using an unrecognized checkpoint layout, carries no derived step and is indistinguishable
309
+ from a genuinely clean run.
310
+ - `xxhash` is now a direct runtime dependency (LangGraph hashes task ids with it, but
311
+ `langgraph` itself is only a development dependency here).
312
+ - `langgraph-checkpoint>=4.1` is now declared directly rather than left transitive, because
313
+ ingestion calls its serializer API. `allowed_msgpack_modules` does not exist before 4.1
314
+ (4.0.0 raises `TypeError`), so the floor is what makes the hardening above guaranteed
315
+ rather than incidental.
316
+
317
+ ### Known limitations
318
+
319
+ - Tasks that were *scheduled but never committed* — a run paused by `interrupt()`, stopped by
320
+ the recursion limit, or killed — are not reported. Trace `status` still means "did a step
321
+ error", so such a run reports `ok`.
322
+ - Only the latest persisted error survives per task, so repeated attempts are not
323
+ reconstructed.
324
+ - `Send` packets can only be named where `langgraph.types` is importable; an ordinary install
325
+ has only the `langgraph.checkpoint` half of that namespace, and the gap is disclosed.
326
+
327
+ ## 0.1.0 - 2026-05-30
328
+
329
+ Tagged but never published. Initial read-path causal-graph analysis of LangGraph agent traces.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-tracegraph
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Time-travel debugging and causal RCA for LangGraph agent traces, on a causal graph.
5
5
  Project-URL: Homepage, https://github.com/memtomem/tracegraph
6
6
  Project-URL: Repository, https://github.com/memtomem/tracegraph
@@ -26,7 +26,7 @@ Requires-Dist: rich>=13
26
26
  Requires-Dist: typer>=0.12
27
27
  Requires-Dist: xxhash>=3
28
28
  Provides-Extra: cypher
29
- Requires-Dist: ladybug<0.19,>=0.18; extra == 'cypher'
29
+ Requires-Dist: ladybug<0.22,>=0.18; extra == 'cypher'
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  # tracegraph
@@ -110,6 +110,9 @@ tracegraph query tool-failure --backend ladybug A.json B.json # use the option
110
110
 
111
111
  # export versioned, body-free governance evidence (an empty match set is a valid report)
112
112
  tracegraph export-review-candidates tool-retry-failure *.json -o candidates.json
113
+
114
+ # export an allowlisted incident post-mortem (Markdown) for a long-term memory store
115
+ tracegraph export-incident-memo A.json -o incident.md
113
116
  ```
114
117
 
115
118
  Frequently-used options, beyond `--out` / `--json-out`:
@@ -273,17 +276,27 @@ result, blast radius, preflight result, or graph state.
273
276
 
274
277
  ## Status
275
278
 
276
- **MVP works** — LangGraph/Phoenix/OTLP ingest plus inspect, explain, analyze, diff, and query.
279
+ Alpha (`0.x`): the artifact, report and review-candidate formats are versioned contracts,
280
+ and any change to them is called out in the [changelog](CHANGELOG.md).
277
281
 
278
- - **Phase 0 (frozen contract):** `RawTrace` vs `NormalizedTrace`, portable JSON artifact (system of record), `validate_raw` → projection → `validate_tree`/`validate_normalized`, in-memory store, `explain`.
279
- - **Phase 1 (ingestion):** `LangGraphCheckpointAdapter` reads any `BaseCheckpointSaver`, including subgraph checkpoint namespaces, and reconstructs declared checkpoint parentage; `examples/tiny_agent.py` generates real traces.
280
- - **Phase 2 (analysis + CLI):** AHU rooted-tree diff and the Typer CLI.
281
- - **Phase 3 (cross-trace queries):** backend-neutral `PathPattern` matcher over the raw causal graph + `query`/`presets` CLI — pure-Python, proving the "graph queries" value before any Cypher backend. The official `tool-retry-failure@v2` requires an explicit producer marker; variable-length **gaps** and **back-references** (`same_name_as`) remain available for query-only heuristics. Uncompilable unbounded patterns degrade honestly rather than truncate.
282
- - **Phase 5 (OTLP/OpenInference adapter):** `OTLPSpanAdapter` ingests Collector JSON/JSONL spans into the same causal model — the source that exercises full link-preserving raw/derived causality.
283
- - **Phoenix diagnosis:** `PhoenixExportAdapter`, `analyze`, and `phoenix diagnose` provide body-free automatic failure selection, retry detection, telemetry/evaluation summaries, and explicit parent-only fidelity warnings.
284
- - **Phase 6 (optional Cypher backend):** `agent-tracegraph[cypher]` ships a `LadybugStore` that compiles the **same** `PathPattern` spec to Cypher (`compile_to_cypher`); equivalence with the pure-Python matcher is the test contract, so the Cypher path is an accelerator, never a second source of truth.
285
- - **Ecosystem T3/P4 review slice:** OTLP `syncmill.run_id` correlation, versioned presets, deterministic body-free `export-review-candidates`, SyncMill human-review board intake, Toolgraph G3 artifact annotation, and the pinned live single-failure review path are complete. Explicit retry causality and SyncMill-to-Phoenix streaming are covered by the controlled E2E workflow; broader operational coverage remains separate.
286
- - **SyncMill contract completion:** route/pipeline/compete/council/decompose plus cancellation fixtures, stable span naming, body-free artifact digests, fail-open exporter reference behavior, operational failure presets, and non-causal Toolgraph preflight evidence are covered by executable tests.
282
+ | Area | What exists |
283
+ |---|---|
284
+ | Sources | LangGraph checkpoint savers (including subgraph namespaces and native task failures), OTLP/OpenInference span exports, Phoenix CLI exports |
285
+ | Commands | `ingest`, `ingest-otlp`, `ingest-phoenix`, `inspect`, `validate`, `explain`, `diff`, `analyze`, `presets`, `query`, `export-review-candidates`, `export-incident-memo`, `phoenix doctor`, `phoenix diagnose` |
286
+ | Patterns | Versioned presets matched over the raw causal graph; the governance-eligible `tool-retry-failure@v2` requires an explicit producer retry marker, and repetition-only heuristics are query-only |
287
+ | Backends | Pure-Python in-memory store (default); optional LadybugDB/Cypher accelerator (`[cypher]`), whose results must equal the pure-Python matcher's |
288
+ | Ecosystem | SyncMill run correlation (`syncmill.run_id`), Toolgraph preflight evidence as non-causal metadata, a weekly Phoenix + SyncMill end-to-end workflow (Phoenix CLI and server pinned; SyncMill tracks its default branch) |
289
+
290
+ Known limits:
291
+
292
+ - A LangGraph task that was scheduled but left no result and no recorded error (a run
293
+ paused by `interrupt()`, stopped by the recursion limit, or killed) is not reported, so
294
+ such a run reads `ok` unless some other step recorded an error.
295
+ - Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
296
+ causes may be missing.
297
+ - Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
298
+ producer's names and error messages.
299
+ - CI covers Python 3.12 on Linux.
287
300
 
288
301
  Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
289
302
  super-step), and node names/kinds are best-effort metadata. Phoenix/OTLP adapters are
@@ -328,14 +341,30 @@ with deterministic SHA-256 aliases, and disclose in `warnings` when an alias act
328
341
  the report. Trace/step identifiers remain; this is not anonymization.
329
342
  Raw artifact names and bytes are not rewritten for report privacy or matching, so the
330
343
  artifact — unlike the report — can still carry whatever the producer put in a span name or
331
- error message (Phoenix ingestion excepted; it filters at the source).
344
+ error message. Phoenix ingestion is the exception: it filters at the source, keeping span
345
+ names and annotation names and labels only when they are identifier-shaped, and dropping
346
+ status messages. Identifier-shaped is not the same as anonymous: an e-mail address passes.
332
347
  `export-review-candidates` output is not passed through the report redactor: its `tool_key`
333
- is the raw endpoint span name. Logical
348
+ is the raw endpoint span name. `export-incident-memo` goes further than `safe-v1`: it writes
349
+ no step names, error messages or evaluation labels at all. Steps appear as kind-based
350
+ aliases and the run id as a truncated digest; the rest is structural metadata such as trace
351
+ status, causal fidelity, step and error counts, preset pattern ids and versions, edge
352
+ origins, fidelity disclosures, and the digest of the exact artifact bytes. Logical
334
353
  comparison keys are JSON-encoded typed paths; consumers should treat them as opaque strings.
335
354
  Containment and unknown edges preserve all error investigation candidates, deeper first.
336
355
  Explicit causal ancestry supplies context, not proof that an exception propagated.
337
356
  Explicit OTLP OK takes precedence over handled exception events on new ingestion; old
338
357
  artifacts are not reinterpreted.
358
+ A span whose status is UNSET and that carries no exception event (one with a message or
359
+ type makes it `error` in both importers) is read differently by source. `ingest-otlp`
360
+ records it as `ok`, because UNSET is OpenTelemetry's default status and producers such as
361
+ SyncMill leave successful spans unset. Phoenix ingestion keeps it as `unset`. Trace status
362
+ is the same either way, but step statuses differ wherever they are shown, and a preset that
363
+ requires an `ok` step does not match an unset one: a SyncMill run where an agent attempt
364
+ left UNSET directly causes a failing `gate:` tool reports `gate-failure-after-success`
365
+ through `ingest-otlp` and not through Phoenix. Compare a run with a baseline from the same
366
+ source; mixing them can report a step whose status differs only by source as a behavior
367
+ change (`ok` to `unset`, or the reverse).
339
368
 
340
369
  LangGraph ingestion reads both the configured error state channel and native task failures
341
370
  from checkpoint pending writes. A node that raises becomes a derived `task` step under the
@@ -346,9 +375,11 @@ borrowing a neighbour's name. When one task raises, LangGraph
346
375
  cancels its siblings and records that through the same channel; a cancelled task is reported
347
376
  as an `unset` step with no message, because it was torn down rather than at fault. Only the
348
377
  latest persisted error survives per task, so repeated attempts are not reconstructed, and
349
- `Send` packets are nameable only where `langgraph.types` is importable — an ordinary install discloses them instead. An artifact carrying derived task
350
- steps declares artifact `schema_version` 3, so an older reader refuses it by name; artifacts
351
- without them still serialize as version 2, byte for byte. Missing observed errors still do not
378
+ `Send` packets are nameable only where `langgraph.types` is importable — an ordinary install
379
+ discloses them instead. An artifact whose derived tree relies on EdgeOrigin precedence declares
380
+ artifact `schema_version` 4; otherwise, an artifact carrying derived task steps declares
381
+ `schema_version` 3, and artifacts without them still serialize as version 2, byte for byte.
382
+ Missing observed errors still do not
352
383
  prove success: an artifact ingested by an older build, or one using an unrecognized checkpoint
353
384
  layout, carries no derived step and looks exactly like a clean run, which is why the capture
354
385
  disclosure is unconditional. Link preservation
@@ -356,7 +387,10 @@ means valid in-trace links only. Structural diff compares the derived tree and d
356
387
  lossy step counts. Metrics sum available span observations: partial coverage and producer
357
388
  aggregation can undercount or double count. Coverage gaps are disclosed rather than absorbed
358
389
  — a wall duration derived from a subset of steps says so, one whose end precedes its start is
359
- withheld, and a single unparseable cost withholds the whole total instead of understating it. File input has a size limit, but stdin/Phoenix
390
+ withheld, and a single unparseable cost withholds the whole total instead of understating it.
391
+ An OTLP or Phoenix import records an unreadable total cost as `unreadable` for that reason, and
392
+ reports token counts, other costs, currencies and evaluation scores it cannot read as
393
+ unavailable, disclosing each on stderr; the producer's value is never kept. File input has a size limit, but stdin/Phoenix
360
394
  subprocess output and query materialization are not streaming memory guarantees.
361
395
 
362
396
  Both stores isolate returned mutable objects. InMemory node upsert replaces by ID;
@@ -79,6 +79,9 @@ tracegraph query tool-failure --backend ladybug A.json B.json # use the option
79
79
 
80
80
  # export versioned, body-free governance evidence (an empty match set is a valid report)
81
81
  tracegraph export-review-candidates tool-retry-failure *.json -o candidates.json
82
+
83
+ # export an allowlisted incident post-mortem (Markdown) for a long-term memory store
84
+ tracegraph export-incident-memo A.json -o incident.md
82
85
  ```
83
86
 
84
87
  Frequently-used options, beyond `--out` / `--json-out`:
@@ -242,17 +245,27 @@ result, blast radius, preflight result, or graph state.
242
245
 
243
246
  ## Status
244
247
 
245
- **MVP works** — LangGraph/Phoenix/OTLP ingest plus inspect, explain, analyze, diff, and query.
248
+ Alpha (`0.x`): the artifact, report and review-candidate formats are versioned contracts,
249
+ and any change to them is called out in the [changelog](CHANGELOG.md).
246
250
 
247
- - **Phase 0 (frozen contract):** `RawTrace` vs `NormalizedTrace`, portable JSON artifact (system of record), `validate_raw` → projection → `validate_tree`/`validate_normalized`, in-memory store, `explain`.
248
- - **Phase 1 (ingestion):** `LangGraphCheckpointAdapter` reads any `BaseCheckpointSaver`, including subgraph checkpoint namespaces, and reconstructs declared checkpoint parentage; `examples/tiny_agent.py` generates real traces.
249
- - **Phase 2 (analysis + CLI):** AHU rooted-tree diff and the Typer CLI.
250
- - **Phase 3 (cross-trace queries):** backend-neutral `PathPattern` matcher over the raw causal graph + `query`/`presets` CLI — pure-Python, proving the "graph queries" value before any Cypher backend. The official `tool-retry-failure@v2` requires an explicit producer marker; variable-length **gaps** and **back-references** (`same_name_as`) remain available for query-only heuristics. Uncompilable unbounded patterns degrade honestly rather than truncate.
251
- - **Phase 5 (OTLP/OpenInference adapter):** `OTLPSpanAdapter` ingests Collector JSON/JSONL spans into the same causal model — the source that exercises full link-preserving raw/derived causality.
252
- - **Phoenix diagnosis:** `PhoenixExportAdapter`, `analyze`, and `phoenix diagnose` provide body-free automatic failure selection, retry detection, telemetry/evaluation summaries, and explicit parent-only fidelity warnings.
253
- - **Phase 6 (optional Cypher backend):** `agent-tracegraph[cypher]` ships a `LadybugStore` that compiles the **same** `PathPattern` spec to Cypher (`compile_to_cypher`); equivalence with the pure-Python matcher is the test contract, so the Cypher path is an accelerator, never a second source of truth.
254
- - **Ecosystem T3/P4 review slice:** OTLP `syncmill.run_id` correlation, versioned presets, deterministic body-free `export-review-candidates`, SyncMill human-review board intake, Toolgraph G3 artifact annotation, and the pinned live single-failure review path are complete. Explicit retry causality and SyncMill-to-Phoenix streaming are covered by the controlled E2E workflow; broader operational coverage remains separate.
255
- - **SyncMill contract completion:** route/pipeline/compete/council/decompose plus cancellation fixtures, stable span naming, body-free artifact digests, fail-open exporter reference behavior, operational failure presets, and non-causal Toolgraph preflight evidence are covered by executable tests.
251
+ | Area | What exists |
252
+ |---|---|
253
+ | Sources | LangGraph checkpoint savers (including subgraph namespaces and native task failures), OTLP/OpenInference span exports, Phoenix CLI exports |
254
+ | Commands | `ingest`, `ingest-otlp`, `ingest-phoenix`, `inspect`, `validate`, `explain`, `diff`, `analyze`, `presets`, `query`, `export-review-candidates`, `export-incident-memo`, `phoenix doctor`, `phoenix diagnose` |
255
+ | Patterns | Versioned presets matched over the raw causal graph; the governance-eligible `tool-retry-failure@v2` requires an explicit producer retry marker, and repetition-only heuristics are query-only |
256
+ | Backends | Pure-Python in-memory store (default); optional LadybugDB/Cypher accelerator (`[cypher]`), whose results must equal the pure-Python matcher's |
257
+ | Ecosystem | SyncMill run correlation (`syncmill.run_id`), Toolgraph preflight evidence as non-causal metadata, a weekly Phoenix + SyncMill end-to-end workflow (Phoenix CLI and server pinned; SyncMill tracks its default branch) |
258
+
259
+ Known limits:
260
+
261
+ - A LangGraph task that was scheduled but left no result and no recorded error (a run
262
+ paused by `interrupt()`, stopped by the recursion limit, or killed) is not reported, so
263
+ such a run reads `ok` unless some other step recorded an error.
264
+ - Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
265
+ causes may be missing.
266
+ - Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
267
+ producer's names and error messages.
268
+ - CI covers Python 3.12 on Linux.
256
269
 
257
270
  Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
258
271
  super-step), and node names/kinds are best-effort metadata. Phoenix/OTLP adapters are
@@ -297,14 +310,30 @@ with deterministic SHA-256 aliases, and disclose in `warnings` when an alias act
297
310
  the report. Trace/step identifiers remain; this is not anonymization.
298
311
  Raw artifact names and bytes are not rewritten for report privacy or matching, so the
299
312
  artifact — unlike the report — can still carry whatever the producer put in a span name or
300
- error message (Phoenix ingestion excepted; it filters at the source).
313
+ error message. Phoenix ingestion is the exception: it filters at the source, keeping span
314
+ names and annotation names and labels only when they are identifier-shaped, and dropping
315
+ status messages. Identifier-shaped is not the same as anonymous: an e-mail address passes.
301
316
  `export-review-candidates` output is not passed through the report redactor: its `tool_key`
302
- is the raw endpoint span name. Logical
317
+ is the raw endpoint span name. `export-incident-memo` goes further than `safe-v1`: it writes
318
+ no step names, error messages or evaluation labels at all. Steps appear as kind-based
319
+ aliases and the run id as a truncated digest; the rest is structural metadata such as trace
320
+ status, causal fidelity, step and error counts, preset pattern ids and versions, edge
321
+ origins, fidelity disclosures, and the digest of the exact artifact bytes. Logical
303
322
  comparison keys are JSON-encoded typed paths; consumers should treat them as opaque strings.
304
323
  Containment and unknown edges preserve all error investigation candidates, deeper first.
305
324
  Explicit causal ancestry supplies context, not proof that an exception propagated.
306
325
  Explicit OTLP OK takes precedence over handled exception events on new ingestion; old
307
326
  artifacts are not reinterpreted.
327
+ A span whose status is UNSET and that carries no exception event (one with a message or
328
+ type makes it `error` in both importers) is read differently by source. `ingest-otlp`
329
+ records it as `ok`, because UNSET is OpenTelemetry's default status and producers such as
330
+ SyncMill leave successful spans unset. Phoenix ingestion keeps it as `unset`. Trace status
331
+ is the same either way, but step statuses differ wherever they are shown, and a preset that
332
+ requires an `ok` step does not match an unset one: a SyncMill run where an agent attempt
333
+ left UNSET directly causes a failing `gate:` tool reports `gate-failure-after-success`
334
+ through `ingest-otlp` and not through Phoenix. Compare a run with a baseline from the same
335
+ source; mixing them can report a step whose status differs only by source as a behavior
336
+ change (`ok` to `unset`, or the reverse).
308
337
 
309
338
  LangGraph ingestion reads both the configured error state channel and native task failures
310
339
  from checkpoint pending writes. A node that raises becomes a derived `task` step under the
@@ -315,9 +344,11 @@ borrowing a neighbour's name. When one task raises, LangGraph
315
344
  cancels its siblings and records that through the same channel; a cancelled task is reported
316
345
  as an `unset` step with no message, because it was torn down rather than at fault. Only the
317
346
  latest persisted error survives per task, so repeated attempts are not reconstructed, and
318
- `Send` packets are nameable only where `langgraph.types` is importable — an ordinary install discloses them instead. An artifact carrying derived task
319
- steps declares artifact `schema_version` 3, so an older reader refuses it by name; artifacts
320
- without them still serialize as version 2, byte for byte. Missing observed errors still do not
347
+ `Send` packets are nameable only where `langgraph.types` is importable — an ordinary install
348
+ discloses them instead. An artifact whose derived tree relies on EdgeOrigin precedence declares
349
+ artifact `schema_version` 4; otherwise, an artifact carrying derived task steps declares
350
+ `schema_version` 3, and artifacts without them still serialize as version 2, byte for byte.
351
+ Missing observed errors still do not
321
352
  prove success: an artifact ingested by an older build, or one using an unrecognized checkpoint
322
353
  layout, carries no derived step and looks exactly like a clean run, which is why the capture
323
354
  disclosure is unconditional. Link preservation
@@ -325,7 +356,10 @@ means valid in-trace links only. Structural diff compares the derived tree and d
325
356
  lossy step counts. Metrics sum available span observations: partial coverage and producer
326
357
  aggregation can undercount or double count. Coverage gaps are disclosed rather than absorbed
327
358
  — a wall duration derived from a subset of steps says so, one whose end precedes its start is
328
- withheld, and a single unparseable cost withholds the whole total instead of understating it. File input has a size limit, but stdin/Phoenix
359
+ withheld, and a single unparseable cost withholds the whole total instead of understating it.
360
+ An OTLP or Phoenix import records an unreadable total cost as `unreadable` for that reason, and
361
+ reports token counts, other costs, currencies and evaluation scores it cannot read as
362
+ unavailable, disclosing each on stderr; the producer's value is never kept. File input has a size limit, but stdin/Phoenix
329
363
  subprocess output and query materialization are not streaming memory guarantees.
330
364
 
331
365
  Both stores isolate returned mutable objects. InMemory node upsert replaces by ID;
@@ -1,5 +1,5 @@
1
1
  {
2
- "phoenix_cli": "1.8.1",
2
+ "phoenix_cli": "1.18.8",
3
3
  "phoenix_server": {
4
4
  "image": "arizephoenix/phoenix",
5
5
  "version": "18.0.0",
@@ -4,7 +4,7 @@
4
4
  # the schema `kind` constants and the TRACEGRAPH_* environment variables — those are package
5
5
  # identity, not distribution identity, and renaming them would break published contracts.
6
6
  name = "agent-tracegraph"
7
- version = "0.2.0"
7
+ version = "0.3.1"
8
8
  description = "Time-travel debugging and causal RCA for LangGraph agent traces, on a causal graph."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -58,7 +58,7 @@ tracegraph = "tracegraph.cli:main"
58
58
  # LadybugDB is the actively maintained successor to the archived Kùzu project. The JSON
59
59
  # artifact remains authoritative, so an accelerator cache is rebuilt across DB versions.
60
60
  [project.optional-dependencies]
61
- cypher = ["ladybug>=0.18,<0.19"]
61
+ cypher = ["ladybug>=0.18,<0.22"]
62
62
 
63
63
  [dependency-groups]
64
64
  dev = [
@@ -70,6 +70,9 @@ dev = [
70
70
  # a dependency bump there would turn that test red for a reason nothing in it names.
71
71
  "pyyaml>=6",
72
72
  ]
73
+ # Kept out of `dev` so the default environment stays what the test jobs prove; CI's lint
74
+ # job installs it explicitly. Pinned: a new mypy release is a new set of rules.
75
+ typing = ["mypy==1.18.2"]
73
76
 
74
77
  [build-system]
75
78
  # Pinned exactly. A release rehearsal and the production tag are two builds on two days, and
@@ -115,6 +118,15 @@ src = ["src", "tests"]
115
118
  select = ["F", "E4", "E7", "E9", "B"]
116
119
  ignore = ["B008"] # typer declares option defaults in the signature by design
117
120
 
121
+ [tool.mypy]
122
+ # The wheel ships py.typed, so its annotations are a contract. Not `strict`: the goal is
123
+ # that what is annotated is checked and correct, not that every helper gains a signature.
124
+ python_version = "3.12"
125
+ files = ["src/tracegraph"]
126
+ check_untyped_defs = true
127
+ warn_unused_ignores = true
128
+ warn_redundant_casts = true
129
+
118
130
  [tool.pytest.ini_options]
119
131
  testpaths = ["tests"]
120
132
  pythonpath = ["examples"]