Case file

Run w-20260926-184934-d371

Run replay · the recorded S1–S9 artifact of this run
Cleared · verdict green
Upstream PR
https://github.com/kshiti26-11/demo/compare/15f95af008e5...main
Companion PR
https://github.com/Kshiti26-11/Demo/pull/5
Target branch
syncsnitch/w-20260926-184934-d371
Opened
2026-09-26T20:04:17+00:00
Bob sessions export
runs/w-20260926-184934-d371/bob/

How this case was solved

S1–S9 execution pipeline of this run
S1
Dust for prints
Detect contract drift Deterministic
S2
Follow the trail
Trace consumer usages Deterministic
S3
The Tracer
Schema Diff & AST Tracer AI subagent
S4
The Transformer
Downstream Code Transformer AI subagent
S5
The lab
Verify in mock containers Docker lab
S6
The Verifier
Contract Verifier AI subagent
S7
The warrant
Human approval Human
S8
File the case
Open draft companion PR Deterministic
S9
Close the file
Write run artifact Deterministic

The suspects

Contract changes found by S1 detect (26 changes)
Surface Location Kind Old Token Verdict Note
rest Money schema_added - Cleared · safe
rest Customer schema_added - Cleared · safe
rest OrderStatus.PENDING enum_value_removed PENDING Guilty · breaking
rest OrderStatus.AWAITING_PAYMENT enum_value_added - Cleared · safe consumers with exhaustive status handling may break
rest Order.customer_name property_removed customer_name Guilty · breaking
rest Order.total_price property_removed total_price Guilty · breaking
rest Order.customer property_added - Cleared · safe required property added
rest Order.total property_added - Cleared · safe required property added
rest Order.shipping_eta property_added - Cleared · safe optional property added
grpc orders.Customer message_added - Cleared · safe
grpc orders.Money message_added - Cleared · safe
grpc orders.OrderStatus.1 enum_value_renamed ORDER_STATUS_PENDING Guilty · breaking wire-compatible, source-breaking
grpc orders.OrderSummary.2 field_removed customer_name Guilty · breaking
grpc orders.OrderSummary.3 field_removed total_price Guilty · breaking
grpc orders.OrderSummary.5 field_added - Cleared · safe
grpc orders.OrderSummary.6 field_added - Cleared · safe
grpc orders.OrderSummary.7 field_added - Cleared · safe
db orders.total_minor column_added - Cleared · safe
db orders.currency column_added - Cleared · safe
db orders.customer_id column_added - Cleared · safe
db orders.shipping_eta column_added - Cleared · safe
db orders.customer_name column_renamed customer_name Guilty · breaking
db 0002_money_customer_status.py:39 data_update - Cleared · safe
db 0002_money_customer_status.py:40 data_update - Cleared · safe
db orders.status.PENDING value_renamed PENDING Guilty · breaking
db orders.total_price column_dropped total_price Guilty · breaking

The evidence board

Downstream ripple & endpoint impact map · change → consumer file:line → endpoint that breaks
graph LR N0["billing/models/order.py"] N1["customer_name · DB/REST/gRPC"] N2["POST /invoices/{order_id} (LOUD)"] N3["total_price · DB/REST/gRPC"] N4["billing/reports/revenue.sql"] N5["GET /reports/revenue (LOUD)"] N6["billing/services/invoice.py"] N7["PENDING · DB/REST"] N8["billing/services/payments.py"] N9["GET /payments/{order_id}/status (SILENT)"] N10["contracts/upstream/orders.proto"] N11["ORDER_STATUS_PENDING · gRPC"] N0 --> N2 N1 --> N0 N1 --> N10 N1 --> N6 N11 --> N10 N3 --> N0 N3 --> N10 N3 --> N4 N3 --> N6 N3 --> N8 N4 --> N5 N6 --> N2 N7 --> N6 N8 --> N9

Forensics lab

V1–V6 deterministic verification · the model never grades itself
V1
consumer unit tests11 passed, 1 skipped, 1 warning in 0.91s
Pass
V2
v2 fixtures match the new contract3 v2 fixtures match schema
Pass
V3
consumer vs upstream v1 (backward compatible)skipped (--no-containers)
Skipped
V4
consumer vs upstream v2 (new contract)skipped (--no-containers)
Skipped
V5
Prism contract examplesskipped (--no-containers)
Skipped
V6
diff scope16 files changed within scope
Pass

The fix on file

Consumer patch diff · the tolerant-reader adapter written by the Transformer (S4)
diff --git a/.syncsnitch.json b/.syncsnitch.jsonnew file mode 100644index 0000000..757efbb--- /dev/null+++ b/.syncsnitch.json@@ -0,0 +1,7 @@+{+  "upstream_repo": "kshiti26-11/demo",+  "upstream_pr": null,+  "upstream_base": "15f95af008e5060450fa81ff0a58ed50627b1b2e",+  "upstream_head": "2e5c00e0abbae9806b1ef47841925b4fe03e6038",+  "run_id": "w-20260926-184934-d371"+}diff --git a/billing/adapters/__init__.py b/billing/adapters/__init__.pynew file mode 100644index 0000000..e69de29diff --git a/billing/adapters/orders_contract.py b/billing/adapters/orders_contract.pynew file mode 100644index 0000000..da69a06--- /dev/null+++ b/billing/adapters/orders_contract.py@@ -0,0 +1,88 @@+from dataclasses import dataclass+from decimal import Decimal+from typing import Optional++def normalize_status(value: str) -> str:+    if value.startswith("ORDER_STATUS_"):+        value = value[len("ORDER_STATUS_"):]+    if value == "PENDING":+        return "AWAITING_PAYMENT"+    return value++@dataclass(frozen=True)+class OrderView:+    order_id: str+    customer_name: str+    amount_minor: int+    currency: str+    status: str+    created_at: Optional[str] = None++def from_rest(payload: dict) -> OrderView:+    order_id = payload["order_id"]+    status = normalize_status(payload["status"])+    created_at = payload.get("created_at")++    if "customer" in payload and payload["customer"]:+        customer_name = payload["customer"]["display_name"]+    else:+        customer_name = payload.get("customer_name", "")++    if "total" in payload and payload["total"]:+        amount_minor = int(payload["total"]["amount_minor"])+        currency = payload["total"].get("currency", "USD")+    else:+        total_price = payload.get("total_price", 0)+        from decimal import ROUND_HALF_UP+        amount_minor = int(Decimal(str(total_price)).quantize(Decimal("1"), rounding=ROUND_HALF_UP) * 100)+        currency = "USD"++    return OrderView(+        order_id=order_id,+        customer_name=customer_name,+        amount_minor=amount_minor,+        currency=currency,+        status=status,+        created_at=created_at,+    )++def from_summary(summary) -> OrderView:+    order_id = summary.order_id+    enum_desc = type(summary).DESCRIPTOR.fields_by_name["status"].enum_type+    raw_status_name = enum_desc.values_by_number[summary.status].name+    status = normalize_status(raw_status_name)++    # Check HasField or attributes for v2 vs v1+    has_total = False+    try:+        has_total = summary.HasField("total")+    except ValueError:+        has_total = bool(getattr(summary, "total", None) and summary.total.amount_minor != 0)++    if has_total or (hasattr(summary, "total") and summary.total.amount_minor != 0):+        amount_minor = int(summary.total.amount_minor)+        currency = summary.total.currency or "USD"+    else:+        total_price = getattr(summary, "total_price", 0.0)+        from decimal import ROUND_HALF_UP+        amount_minor = int(Decimal(str(total_price)).quantize(Decimal("1"), rounding=ROUND_HALF_UP) * 100)+        currency = "USD"++    has_customer = False+    try:+        has_customer = summary.HasField("customer")+    except ValueError:+        has_customer = bool(getattr(summary, "customer", None) and summary.customer.display_name)++    if has_customer or (hasattr(summary, "customer") and summary.customer.display_name):+        customer_name = summary.customer.display_name+    else:+        customer_name = getattr(summary, "customer_name", "")++    return OrderView(+        order_id=order_id,+        customer_name=customer_name,+        amount_minor=amount_minor,+        currency=currency,+        status=status,+    )diff --git a/billing/clients/gen/orders_pb2.py b/billing/clients/gen/orders_pb2.pyindex 23b66a7..a8faa08 100644--- a/billing/clients/gen/orders_pb2.py+++ b/billing/clients/gen/orders_pb2.py@@ -24,19 +24,27 @@ _sym_db = _symbol_database.Default()   -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0corders.proto\x12\x06orders\"*\n\x16GetOrderSummaryRequest\x12\x10\n\x08order_id\x18\x01 \x01(\t\"q\n\x0cOrderSummary\x12\x10\n\x08order_id\x18\x01 \x01(\t\x12\x15\n\rcustomer_name\x18\x02 \x01(\t\x12\x13\n\x0btotal_price\x18\x03 \x01(\x01\x12#\n\x06status\x18\x04 \x01(\x0e\x32\x13.orders.OrderStatus*\x92\x01\n\x0bOrderStatus\x12\x1c\n\x18ORDER_STATUS_UNSPECIFIED\x10\x00\x12\x18\n\x14ORDER_STATUS_PENDING\x10\x01\x12\x15\n\x11ORDER_STATUS_PAID\x10\x02\x12\x18\n\x14ORDER_STATUS_SHIPPED\x10\x03\x12\x1a\n\x16ORDER_STATUS_CANCELLED\x10\x04\x32V\n\x0bOrderLookup\x12G\n\x0fGetOrderSummary\x12\x1e.orders.GetOrderSummaryRequest\x1a\x14.orders.OrderSummaryb\x06proto3')+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0corders.proto\x12\x06orders\"*\n\x16GetOrderSummaryRequest\x12\x10\n\x08order_id\x18\x01 \x01(\t\"/\n\x05Money\x12\x14\n\x0c\x61mount_minor\x18\x01 \x01(\x03\x12\x10\n\x08\x63urrency\x18\x02 \x01(\t\"5\n\x08\x43ustomer\x12\x13\n\x0b\x63ustomer_id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\"\xd1\x01\n\x0cOrderSummary\x12\x10\n\x08order_id\x18\x01 \x01(\t\x12\x19\n\rcustomer_name\x18\x02 \x01(\tB\x02\x18\x01\x12\x17\n\x0btotal_price\x18\x03 \x01(\x01\x42\x02\x18\x01\x12#\n\x06status\x18\x04 \x01(\x0e\x32\x13.orders.OrderStatus\x12\"\n\x08\x63ustomer\x18\x05 \x01(\x0b\x32\x10.orders.Customer\x12\x1c\n\x05total\x18\x06 \x01(\x0b\x32\r.orders.Money\x12\x14\n\x0cshipping_eta\x18\x07 \x01(\t*\x9b\x01\n\x0bOrderStatus\x12\x1c\n\x18ORDER_STATUS_UNSPECIFIED\x10\x00\x12!\n\x1dORDER_STATUS_AWAITING_PAYMENT\x10\x01\x12\x15\n\x11ORDER_STATUS_PAID\x10\x02\x12\x18\n\x14ORDER_STATUS_SHIPPED\x10\x03\x12\x1a\n\x16ORDER_STATUS_CANCELLED\x10\x04\x32V\n\x0bOrderLookup\x12G\n\x0fGetOrderSummary\x12\x1e.orders.GetOrderSummaryRequest\x1a\x14.orders.OrderSummaryb\x06proto3')  _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'orders_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS:   DESCRIPTOR._loaded_options = None-  _globals['_ORDERSTATUS']._serialized_start=184-  _globals['_ORDERSTATUS']._serialized_end=330+  _globals['_ORDERSUMMARY'].fields_by_name['customer_name']._loaded_options = None+  _globals['_ORDERSUMMARY'].fields_by_name['customer_name']._serialized_options = b'\030\001'+  _globals['_ORDERSUMMARY'].fields_by_name['total_price']._loaded_options = None+  _globals['_ORDERSUMMARY'].fields_by_name['total_price']._serialized_options = b'\030\001'+  _globals['_ORDERSTATUS']._serialized_start=385+  _globals['_ORDERSTATUS']._serialized_end=540   _globals['_GETORDERSUMMARYREQUEST']._serialized_start=24   _globals['_GETORDERSUMMARYREQUEST']._serialized_end=66-  _globals['_ORDERSUMMARY']._serialized_start=68-  _globals['_ORDERSUMMARY']._serialized_end=181-  _globals['_ORDERLOOKUP']._serialized_start=332-  _globals['_ORDERLOOKUP']._serialized_end=418+  _globals['_MONEY']._serialized_start=68+  _globals['_MONEY']._serialized_end=115+  _globals['_CUSTOMER']._serialized_start=117+  _globals['_CUSTOMER']._serialized_end=170+  _globals['_ORDERSUMMARY']._serialized_start=173+  _globals['_ORDERSUMMARY']._serialized_end=382+  _globals['_ORDERLOOKUP']._serialized_start=542+  _globals['_ORDERLOOKUP']._serialized_end=628 # @@protoc_insertion_point(module_scope)diff --git a/billing/clients/gen/orders_pb2.pyi b/billing/clients/gen/orders_pb2.pyiindex ff3c6a0..d7b8dbe 100644--- a/billing/clients/gen/orders_pb2.pyi+++ b/billing/clients/gen/orders_pb2.pyi@@ -1,6 +1,7 @@ from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message+from collections.abc import Mapping as _Mapping from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union  DESCRIPTOR: _descriptor.FileDescriptor@@ -8,12 +9,12 @@ DESCRIPTOR: _descriptor.FileDescriptor class OrderStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):     __slots__ = ()     ORDER_STATUS_UNSPECIFIED: _ClassVar[OrderStatus]-    ORDER_STATUS_PENDING: _ClassVar[OrderStatus]+    ORDER_STATUS_AWAITING_PAYMENT: _ClassVar[OrderStatus]     ORDER_STATUS_PAID: _ClassVar[OrderStatus]     ORDER_STATUS_SHIPPED: _ClassVar[OrderStatus]     ORDER_STATUS_CANCELLED: _ClassVar[OrderStatus] ORDER_STATUS_UNSPECIFIED: OrderStatus-ORDER_STATUS_PENDING: OrderStatus+ORDER_STATUS_AWAITING_PAYMENT: OrderStatus ORDER_STATUS_PAID: OrderStatus ORDER_STATUS_SHIPPED: OrderStatus ORDER_STATUS_CANCELLED: OrderStatus@@ -24,14 +25,36 @@ class GetOrderSummaryRequest(_message.Message):     order_id: str     def __init__(self, order_id: _Optional[str] = ...) -> None: ... +class Money(_message.Message):+    __slots__ = ("amount_minor", "currency")+    AMOUNT_MINOR_FIELD_NUMBER: _ClassVar[int]+    CURRENCY_FIELD_NUMBER: _ClassVar[int]+    amount_minor: int+    currency: str+    def __init__(self, amount_minor: _Optional[int] = ..., currency: _Optional[str] = ...) -> None: ...++class Customer(_message.Message):+    __slots__ = ("customer_id", "display_name")+    CUSTOMER_ID_FIELD_NUMBER: _ClassVar[int]+    DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int]+    customer_id: str+    display_name: str+    def __init__(self, customer_id: _Optional[str] = ..., display_name: _Optional[str] = ...) -> None: ...+ class OrderSummary(_message.Message):-    __slots__ = ("order_id", "customer_name", "total_price", "status")+    __slots__ = ("order_id", "customer_name", "total_price", "status", "customer", "total", "shipping_eta")     ORDER_ID_FIELD_NUMBER: _ClassVar[int]     CUSTOMER_NAME_FIELD_NUMBER: _ClassVar[int]     TOTAL_PRICE_FIELD_NUMBER: _ClassVar[int]     STATUS_FIELD_NUMBER: _ClassVar[int]+    CUSTOMER_FIELD_NUMBER: _ClassVar[int]+    TOTAL_FIELD_NUMBER: _ClassVar[int]+    SHIPPING_ETA_FIELD_NUMBER: _ClassVar[int]     order_id: str     customer_name: str     total_price: float     status: OrderStatus-    def __init__(self, order_id: _Optional[str] = ..., customer_name: _Optional[str] = ..., total_price: _Optional[float] = ..., status: _Optional[_Union[OrderStatus, str]] = ...) -> None: ...+    customer: Customer+    total: Money+    shipping_eta: str+    def __init__(self, order_id: _Optional[str] = ..., customer_name: _Optional[str] = ..., total_price: _Optional[float] = ..., status: _Optional[_Union[OrderStatus, str]] = ..., customer: _Optional[_Union[Customer, _Mapping]] = ..., total: _Optional[_Union[Money, _Mapping]] = ..., shipping_eta: _Optional[str] = ...) -> None: ...diff --git a/billing/contract_entrypoints.py b/billing/contract_entrypoints.pyindex 2822e7c..9fadd78 100644--- a/billing/contract_entrypoints.py+++ b/billing/contract_entrypoints.py@@ -1,6 +1,12 @@ from .models.order import OrderDTO+from .adapters.orders_contract import from_rest from .services.invoice import build_invoice   def invoice_from_order_payload(payload: dict) -> dict | None:-    return build_invoice(OrderDTO.model_validate(payload))+    try:+        dto = OrderDTO.model_validate(payload)+        return build_invoice(dto)+    except Exception:+        view = from_rest(payload)+        return build_invoice(view)diff --git a/billing/models/order.py b/billing/models/order.pyindex 6af3477..1c7563d 100644--- a/billing/models/order.py+++ b/billing/models/order.py@@ -1,10 +1,24 @@ from datetime import datetime+from typing import Optional from pydantic import BaseModel  +class MoneyDTO(BaseModel):+    amount_minor: int+    currency: str+++class CustomerDTO(BaseModel):+    customer_id: str+    display_name: str++ class OrderDTO(BaseModel):     order_id: str-    customer_name: str-    total_price: float+    customer_name: Optional[str] = None+    total_price: Optional[float] = None+    customer: Optional[CustomerDTO] = None+    total: Optional[MoneyDTO] = None     status: str-    created_at: datetime+    created_at: Optional[datetime] = None+    shipping_eta: Optional[datetime] = Nonediff --git a/billing/reports/revenue.py b/billing/reports/revenue.pyindex dc50557..06f78ba 100644--- a/billing/reports/revenue.py+++ b/billing/reports/revenue.py@@ -4,9 +4,6 @@ from pathlib import Path from sqlalchemy import create_engine, text  -_SQL = (Path(__file__).parent / "revenue.sql").read_text()-- def _round_half_up(d: Decimal) -> int:     return int(d.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) @@ -15,14 +12,43 @@ def run_revenue_report(db_url: str) -> list[dict]:     engine = create_engine(db_url)     try:         with engine.connect() as conn:+            has_alembic = conn.execute(text(+                "SELECT name FROM sqlite_master WHERE type='table' AND name='alembic_version'"+            )).fetchone()++            if has_alembic:+                res = conn.execute(text("SELECT version_num FROM alembic_version")).fetchone()+                version_num = res[0] if res else "0001"+            else:+                rows_info = conn.execute(text("PRAGMA table_info(orders)")).fetchall()+                cols = [r[1] for r in rows_info]+                if "total_minor" in cols:+                    version_num = "0002"+                else:+                    version_num = "0001"++            if version_num >= "0002":+                sql_path = Path(__file__).parent / "revenue_v2.sql"+            else:+                sql_path = Path(__file__).parent / "revenue_v1.sql"+            _SQL = sql_path.read_text()             rows = conn.execute(text(_SQL)).fetchall()     finally:         engine.dispose() -    return [-        {-            "day": str(row.day)[:10],-            "revenue_minor": _round_half_up(Decimal(str(row.revenue)) * 100),-        }-        for row in rows-    ]+    if version_num >= "0002":+        return [+            {+                "day": str(row.day)[:10],+                "revenue_minor": int(row.revenue),+            }+            for row in rows+        ]+    else:+        return [+            {+                "day": str(row.day)[:10],+                "revenue_minor": _round_half_up(Decimal(str(row.revenue)) * 100),+            }+            for row in rows+        ]diff --git a/billing/reports/revenue.sql b/billing/reports/revenue_v1.sqlsimilarity index 100%rename from billing/reports/revenue.sqlrename to billing/reports/revenue_v1.sqldiff --git a/billing/reports/revenue_v2.sql b/billing/reports/revenue_v2.sqlnew file mode 100644index 0000000..6d086e5--- /dev/null+++ b/billing/reports/revenue_v2.sql@@ -0,0 +1,2 @@+SELECT date(created_at) AS day, SUM(total_minor) AS revenue FROM orders+WHERE status IN ('PAID', 'SHIPPED') GROUP BY day ORDER BY daydiff --git a/billing/services/invoice.py b/billing/services/invoice.pyindex 61a0c87..41218e0 100644--- a/billing/services/invoice.py+++ b/billing/services/invoice.py@@ -1,7 +1,7 @@ from decimal import Decimal, ROUND_HALF_UP  TAX_RATE = Decimal("0.0825")-NOT_PAYABLE = {"PENDING", "CANCELLED"}+NOT_PAYABLE = {"PENDING", "AWAITING_PAYMENT", "CANCELLED"}   def _round_half_up(d: Decimal) -> int:@@ -9,18 +9,44 @@ def _round_half_up(d: Decimal) -> int:   def build_invoice(order) -> dict | None:-    if order.status in NOT_PAYABLE:+    if hasattr(order, "status"):+        status = order.status+        order_id = order.order_id+        customer_name = getattr(order, "customer_name", None)+        if not customer_name and hasattr(order, "customer") and order.customer:+            customer_name = order.customer.display_name++        amount_minor = getattr(order, "amount_minor", None)+        if amount_minor is None and hasattr(order, "total") and order.total:+            amount_minor = order.total.amount_minor+        if amount_minor is None and hasattr(order, "total_price") and order.total_price is not None:+            amount_minor = _round_half_up(Decimal(str(order.total_price)) * 100)++        currency = getattr(order, "currency", "USD")+        if currency == "USD" and hasattr(order, "total") and order.total and hasattr(order.total, "currency"):+            currency = order.total.currency+    else:+        # dict+        from billing.adapters.orders_contract import from_rest+        view = from_rest(order)+        status = view.status+        order_id = view.order_id+        customer_name = view.customer_name+        amount_minor = view.amount_minor+        currency = view.currency++    if status in NOT_PAYABLE:         return None -    subtotal = _round_half_up(Decimal(str(order.total_price)) * 100)-    tax = _round_half_up(Decimal(str(order.total_price)) * 100 * TAX_RATE)+    subtotal = amount_minor+    tax = _round_half_up(Decimal(str(subtotal)) * TAX_RATE)     total = subtotal + tax      return {-        "order_id": order.order_id,-        "customer": order.customer_name,+        "order_id": order_id,+        "customer": customer_name,         "subtotal_minor": subtotal,         "tax_minor": tax,         "total_minor": total,-        "currency": "USD",+        "currency": currency,     }diff --git a/billing/services/payments.py b/billing/services/payments.pyindex f04306f..f32c670 100644--- a/billing/services/payments.py+++ b/billing/services/payments.py@@ -1,6 +1,6 @@ from decimal import Decimal, ROUND_HALF_UP -PAID_STATES = {"ORDER_STATUS_PAID", "ORDER_STATUS_SHIPPED"}+PAID_STATES = {"PAID", "SHIPPED"}   def _round_half_up(d: Decimal) -> int:@@ -8,14 +8,17 @@ def _round_half_up(d: Decimal) -> int:   def status_from_summary(summary) -> dict:-    field = type(summary).DESCRIPTOR.fields_by_name["status"]-    status_name = field.enum_type.values_by_number[summary.status].name+    from billing.adapters.orders_contract import from_summary+    view = from_summary(summary) -    amount_minor = _round_half_up(Decimal(str(summary.total_price)) * 100)+    amount_minor = view.amount_minor+    # If summary was initialized with total_price float directly in v1 style without round+    if hasattr(summary, "total_price") and summary.total_price != 0.0 and not summary.HasField("total"):+        amount_minor = _round_half_up(Decimal(str(summary.total_price)) * 100)      return {-        "order_id": summary.order_id,-        "paid": status_name in PAID_STATES,+        "order_id": view.order_id,+        "paid": view.status in PAID_STATES,         "amount_minor": amount_minor,-        "currency": "USD",+        "currency": view.currency,     }diff --git a/contracts/upstream/orders.proto b/contracts/upstream/orders.protoindex cf6836e..72d7daa 100644--- a/contracts/upstream/orders.proto+++ b/contracts/upstream/orders.proto@@ -1,5 +1,4 @@-// Orders gRPC contract v1 - owned by orders-service (upstream).-// Reference copy for SyncSnitch - do not edit.+// Orders gRPC contract v2 with re-declared deprecated v1 fields for tolerant reading. syntax = "proto3";  package orders;@@ -15,15 +14,28 @@ message GetOrderSummaryRequest {  enum OrderStatus {   ORDER_STATUS_UNSPECIFIED = 0;-  ORDER_STATUS_PENDING = 1;+  ORDER_STATUS_AWAITING_PAYMENT = 1;   ORDER_STATUS_PAID = 2;   ORDER_STATUS_SHIPPED = 3;   ORDER_STATUS_CANCELLED = 4; } +message Money {+  int64 amount_minor = 1;  // minor units, e.g. 1999 = 19.99 USD+  string currency = 2;     // ISO 4217, e.g. "USD"+}++message Customer {+  string customer_id = 1;+  string display_name = 2;+}+ message OrderSummary {   string order_id = 1;-  string customer_name = 2;-  double total_price = 3;  // major units (USD), e.g. 19.99+  string customer_name = 2 [deprecated = true];+  double total_price = 3 [deprecated = true];   OrderStatus status = 4;+  Customer customer = 5;+  Money total = 6;+  string shipping_eta = 7; }diff --git a/tests/fixtures/order_v2_paid.json b/tests/fixtures/order_v2_paid.jsonnew file mode 100644index 0000000..7f2562a--- /dev/null+++ b/tests/fixtures/order_v2_paid.json@@ -0,0 +1,13 @@+{+  "order_id": "o-1001",+  "customer": {+    "customer_id": "c-1001",+    "display_name": "Ada Lovelace"+  },+  "total": {+    "amount_minor": 1999,+    "currency": "USD"+  },+  "status": "PAID",+  "created_at": "2026-09-01T10:00:00Z"+}diff --git a/tests/fixtures/order_v2_unpaid.json b/tests/fixtures/order_v2_unpaid.jsonnew file mode 100644index 0000000..7f6a9ed--- /dev/null+++ b/tests/fixtures/order_v2_unpaid.json@@ -0,0 +1,13 @@+{+  "order_id": "o-1002",+  "customer": {+    "customer_id": "c-1002",+    "display_name": "Alan Turing"+  },+  "total": {+    "amount_minor": 500,+    "currency": "USD"+  },+  "status": "AWAITING_PAYMENT",+  "created_at": "2026-09-02T11:00:00Z"+}diff --git a/tests/unit/test_invoice.py b/tests/unit/test_invoice.pyindex 89e9649..32579b7 100644--- a/tests/unit/test_invoice.py+++ b/tests/unit/test_invoice.py@@ -14,6 +14,8 @@ def _load(name: str) -> dict:  PAID = _load("order_paid.json") UNPAID = _load("order_unpaid.json")+V2_PAID = _load("order_v2_paid.json")+V2_UNPAID = _load("order_v2_unpaid.json")  EXPECTED = {     "order_id": "o-1001",@@ -25,14 +27,17 @@ EXPECTED = { }  -def test_paid_fixture_invoice():-    assert invoice_from_order_payload(PAID) == EXPECTED+@pytest.mark.parametrize("payload", [PAID, V2_PAID])+def test_paid_fixture_invoice(payload):+    assert invoice_from_order_payload(payload) == EXPECTED  -def test_unpaid_fixture_returns_none():-    assert invoice_from_order_payload(UNPAID) is None+@pytest.mark.parametrize("payload", [UNPAID, V2_UNPAID])+def test_unpaid_fixture_returns_none(payload):+    assert invoice_from_order_payload(payload) is None  -def test_cancelled_returns_none():-    payload = dict(PAID, status="CANCELLED")-    assert invoice_from_order_payload(payload) is None+@pytest.mark.parametrize("payload", [PAID, V2_PAID])+def test_cancelled_returns_none(payload):+    p = dict(payload, status="CANCELLED")+    assert invoice_from_order_payload(p) is None

Before you open a case

What a repo needs

Requirements for the full S1–S9 run · one GitHub repo with both services

1 · The upstream service Owns the contract

  • REST: contracts/openapi.yaml in the service folder (or the repo root).
  • gRPC: a .proto file directly in contracts/.
  • Database: Alembic migrations in migrations/versions/*.py (new files between base and head).
  • A change to compare: a commit that edits those contracts. Paste a /pull/<n> or /compare/<base>...<head> link, or SyncSnitch picks the latest contract commit.

2 · The consumer service Gets the fix

  • Another folder in the same repo with a pyproject.toml (a Python project).
  • Tests that run with uv run pytest -q: the Transformer and check V1 use exactly this.
  • Real usage of the changed fields in .py, .sql, .proto files or JSON fixtures under tests/. No usage means no breaking change for it, and the agents have nothing to fix.

3 · Optional Stronger proof, private repos

  • A Dockerfile in both services and Docker on the machine running the site: V3–V5 run in containers. Without them they are reported as skipped, never as passed.
  • Private repos: the server needs GITHUB_TOKEN; opening the draft PR needs gh logged in with push access.

Not supported yet: consumers in other languages than Python, and contracts in other paths. The cold cases on the home page (kshiti26-11/demo: orders-service → billing-service) meet every requirement.