Migration Guide
spanforge.migrate provides helpers for upgrading stored event payloads
to use new namespace payload schemas, plus the Phase 9 v2 migration roadmap
so you can prepare for breaking changes before v2.0 ships.
MigrationStats
Bulk migration operations return a MigrationStats dataclass:
from spanforge.migrate import MigrationStats
stats: MigrationStats # returned by migrate_file()
stats.total # total events processed
stats.migrated # events upgraded to target version
stats.skipped # events already at target version
stats.errors # events that failed to parse/migrate
stats.warnings # list of non-fatal warning strings
stats.output_path # path to the output file
stats.transformed_fields # {"payload.model→model_id": 5, "checksum.md5→sha256": 2, ...}
Migrating v1.0 → v2.0
v1_to_v2() converts a single event from schema version 1.0 to 2.0.
It accepts both Event instances and plain dict objects (from JSONL):
from spanforge.migrate import v1_to_v2
# Migrate an Event instance
v2_event = v1_to_v2(v1_event)
assert v2_event.schema_version == "2.0"
# Migrate a raw dict from JSONL
v2_dict = v1_to_v2({"schema_version": "1.0", "event_type": "llm.trace.span.completed", ...})
assert v2_dict["schema_version"] == "2.0"
Changes applied:
schema_versionset to"2.0"- Missing
org_id/team_idset toNone - Payload
modelnormalised tomodel_id tagscoerced: all values become strings, initialised to{}if missingchecksumre-hashed from md5 to sha256 if applicable
The function is idempotent — events whose schema_version is already
"2.0" are returned unchanged.
Bulk migration from JSONL
Use migrate_file() for bulk JSONL-to-JSONL migration:
from spanforge.migrate import migrate_file
# Basic migration
stats = migrate_file("audit.jsonl")
print(f"Migrated {stats.migrated}/{stats.total} events → {stats.output_path}")
print(f"Skipped: {stats.skipped}, Errors: {stats.errors}")
print(f"Transformed fields: {stats.transformed_fields}")
Re-signing the migrated chain
Pass org_secret to re-sign the entire output chain with HMAC:
stats = migrate_file("audit.jsonl", output="audit_v2.jsonl", org_secret="my-signing-key")
Dry run (preview without writing)
stats = migrate_file("audit.jsonl", dry_run=True)
print(f"Would migrate {stats.migrated} events, skip {stats.skipped}")
CLI migration
# Basic migration
spanforge migrate audit.jsonl
# With signing and explicit output
spanforge migrate audit.jsonl --sign --target-version 2.0 -o audit_v2.jsonl
# Preview only
spanforge migrate audit.jsonl --dry-run
v2 Migration Roadmap
Phase 9 ships a structured roadmap of every event type that will change
in v2.0. Use v2_migration_roadmap() to audit your codebase:
from spanforge.migrate import v2_migration_roadmap
for record in v2_migration_roadmap():
print(record.summary())
Example output:
llm.cache.evicted → llm.cache.entry_evicted (since 1.0.0, sunset 2.0.0, NEXT_MAJOR)
llm.cost.estimate → llm.cost.estimated (since 1.0.0, sunset 2.0.0, NEXT_MAJOR)
llm.eval.regression → llm.eval.regression_failed (since 1.0.0, sunset 2.0.0, NEXT_MAJOR)
...
Each DeprecationRecord provides:
| Field | Description |
|---|---|
event_type | The deprecated event type string |
since | Version deprecated ("1.0.0") |
sunset | Planned removal version ("2.0.0") |
sunset_policy | SunsetPolicy.NEXT_MAJOR for all roadmap entries |
replacement | Recommended new event type |
migration_notes | Guidance text |
field_renames | {old_field: new_field} dict for payload field renames |
CLI roadmap view
# Human-readable table
spanforge migration-roadmap
# JSON for tooling
spanforge migration-roadmap --json
Sunset policy
| Policy | Meaning |
|---|---|
NEXT_MAJOR | Removed in v2.0.0 |
NEXT_MINOR | Removed in the next minor release |
LONG_TERM | Removed in v3.0.0 or later |
UNSCHEDULED | Advisory deprecation; no removal planned |
All Phase 9 roadmap entries use NEXT_MAJOR — they will be removed when v2.0
ships.
Deprecation warnings
At import time, spanforge.deprecations auto-populates the global
DeprecationRegistry with every entry from v2_migration_roadmap(). Callers
can surface these warnings at runtime:
from spanforge.deprecations import warn_if_deprecated
# Inside event processing loops, or at schema validation time:
warn_if_deprecated(event.event_type)
# → emits DeprecationWarning if this type is on the roadmap
CLI deprecation list
spanforge list-deprecated
Preparing for v2.0
- Run
spanforge migration-roadmapto see the full list. - Search your code for deprecated event type strings.
- Replace with the recommended
replacementtype from each record. - Apply any
field_renamesto affected payload construction sites. - Update consumer registry entries with
schema_version="2.0"once v2.0 ships.
@dataclass
class MigrationResult:
migrated: list[Event] # successfully transformed events
skipped: list[Event] # events that needed no change
errors: list[dict] # {"event_id": str, "error": str}
@property
def success(self) -> bool:
return len(self.errors) == 0
Migrating v1 → v2 (scaffold)
The v1_to_v2 scaffold converts events recorded with the llm.trace.*
payload from the frozen v1.0 schema to any updated v2 layout that ships in
Phase 9:
from spanforge.migrate import v1_to_v2
result = v1_to_v2(events)
if result.success:
save(result.migrated)
else:
for err in result.errors:
print(f"{err['event_id']}: {err['error']}")
The function is idempotent — events whose schema_version is already
"2.0" are placed in result.skipped unchanged.
Batch migration from JSONL
Read a JSONL archive, migrate, and write the output:
import json
from spanforge.event import Event
from spanforge.migrate import v1_to_v2
events = [Event(**json.loads(line)) for line in open("archive.jsonl")]
result = v1_to_v2(events)
with open("archive_v2.jsonl", "w") as f:
for event in result.migrated + result.skipped:
f.write(json.dumps(event.to_dict()) + "\n")
print(f"Migrated: {len(result.migrated)}")
print(f"Skipped: {len(result.skipped)}")
print(f"Errors: {len(result.errors)}")
Phase 9 roadmap
Phase 9 will ship breaking-change namespace payload schemas alongside a
migrate sub-command for the CLI:
spanforge migrate --from v1 --to v2 archive.jsonl --out archive_v2.jsonl
Until Phase 9 ships, the v1_to_v2 Python API is the primary migration path.
The function signature and MigrationResult dataclass fields are considered
stable and will not change in Phase 9.