Skip to content

Commit 26bc526

Browse files
committed
merge: M3-W124 -- a synthetic shape's counter holds where a traffic counter adds
2 parents 264f346 + 907c06c commit 26bc526

7 files changed

Lines changed: 711 additions & 20 deletions

File tree

docs/superpowers/reports/2026-08-03-a-retried-replay-converges.md

Lines changed: 448 additions & 0 deletions
Large diffs are not rendered by default.

docs/superpowers/specs/2026-07-26-sync-observed-contract-drift.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@ and `src/sync/verify/mock_response.py`, wired as the `replay` node in
1212
`src/sync/remediate/graph.py:37`) and does not feed the shape store — deliberately, which
1313
withdraws this document's claim below that every replay run is also a `source='replay'` writer.
1414
A replay row is the published specification restated through the customer's code, not a response
15-
the customer's code received, and two consumers read this table as traffic: one replay row is
16-
enough to turn an uncorroborated divergence into a `breaking` finding, and a row at the floor
17-
outranks the specification in the mock the next replay is verified against.
18-
`docs/superpowers/reports/2026-07-30-replay-shapes-reach-the-store.md` carries the measurements
19-
and names what `src/sync/detect/` and `src/sync/graph/` have to change before replay can write.
15+
the customer's code received. Two consumers used to read this table as traffic with no way to say
16+
so, and both conditions that blocked the writer have since been answered:
17+
`GraphStore.observed_shapes` returns traffic alone unless a caller asks for every source, and
18+
`record_observed_shape` no longer accumulates `sample_count` for a synthetic source, so a replay
19+
retried or repeated converges instead of counting one synthesized body once per attempt.
20+
The writer is still not reinstated — that is its own task, and what it now owes is a consumer,
21+
since no caller in `src/` reads a `replay` row.
22+
`docs/superpowers/reports/2026-07-30-replay-shapes-reach-the-store.md` carries the measurements,
23+
`docs/superpowers/reports/2026-07-31-traffic-and-non-traffic-shapes.md` closed the first
24+
condition and `docs/superpowers/reports/2026-08-03-a-retried-replay-converges.md` the second.
2025
The interceptor SDK does not exist — see Sequencing.
2126
**Scope:** Detecting vendor changes no one published, and verifying patches against behavior rather than types
2227
alone. The design transposes Meticulous's record-replay-diff mechanism into the API-consumption domain.
@@ -205,7 +210,7 @@ number remains in engineering docs; the volume number is the one a customer sees
205210
| When | What | State |
206211
|---|---|---|
207212
| Now | This document. The `observed_shape` schema is binding on anything that later records shapes. | Built — `src/sync/graph/schema.sql:124`, `ObservedShape` in `src/sync/core/models.py` |
208-
| M1 (with the sandbox the threat model gates on) | The replay tier. Feeding the shape store as `source='replay'` was specified here and is withdrawn. | Built as a verification stage, and deliberately not a feeder — `src/sync/verify/replay.py`, between `static_verify` and `push_branch` in `src/sync/remediate/graph.py`. Its `source='replay'` rows reach `RunState` and no further, pinned by `tests/test_replay_shape_writeback.py`. Replay rows are synthesized from the specification, and `observed_shapes` has no `source` filter, so they would reach two consumers that read the table as traffic. Reinstating the writer is blocked on `src/sync/detect/` and `src/sync/graph/` — see the report of 2026-07-30 |
213+
| M1 (with the sandbox the threat model gates on) | The replay tier. Feeding the shape store as `source='replay'` was specified here and is withdrawn. | Built as a verification stage, and deliberately not a feeder — `src/sync/verify/replay.py`, between `static_verify` and `push_branch` in `src/sync/remediate/graph.py`. Its `source='replay'` rows reach `RunState` and no further, pinned by `tests/test_replay_shape_writeback.py`. Both conditions the report of 2026-07-30 set are answered: traffic and synthetic rows are kept apart on read (2026-07-31), and a synthetic row's `sample_count` no longer accumulates, so a retried replay converges (2026-08-03). Reinstating the writer is still its own task, and it owes a consumer for these rows — both readers now answer traffic alone |
209214
| M2 (signal sources) | Error-payload shapes from Sentry-class sources, `source='error-payload'`. The detector ships here, running on whatever baseline exists, with the sample floor keeping it silent where data is thin. | Built — `src/sync/signals/sentry/shapes.py`, `src/sync/signals/datadog/shapes.py`, and `src/sync/detect/observed_drift.py`, whose `MIN_SAMPLES` is the sample floor |
210215
| Post-M2, opt-in | The interceptor SDK, only for customers who want unpublished-change detection on live traffic. A separate adoption decision with its own trust conversation. | Not built |
211216

src/sync/graph/schema.sql

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,25 @@ CREATE INDEX IF NOT EXISTS migration_outcome_kind_idx
242242
-- column exists at all: a table that appended would make every presence rate a function of how
243243
-- often the ingest ran rather than of what the vendor sent.
244244
--
245+
-- **sample_count means one thing per source class and merges accordingly**, and it is the
246+
-- sentence above that forces the split rather than an exception to it. For a traffic source the
247+
-- write is a response somebody received, so a second write is a second sample and the counter
248+
-- adds. For a synthetic source -- `sync.graph.sources.SYNTHETIC_SOURCES`, the rows Sync
249+
-- constructed from a published specification -- a second write is the ingest running again over
250+
-- the same constructed body, so the counter holds: it is the largest single claim made about the
251+
-- row rather than the sum of the claims. One row still means one shape; for a synthetic source
252+
-- it also means one sample, at any repetition count.
253+
--
254+
-- The replay tier is what forces it. A failed replay re-enters `patch` and MAX_STATIC_ATTEMPTS
255+
-- is 3, so one finding offers the same synthesized body three times, and ten findings over one
256+
-- operation would carry it past the sample floor the drift detector reads. A run key on the row
257+
-- does not fix that -- it changes which row the addition lands in, and the retry writes the same
258+
-- key twice. Convergence under re-execution is a property of the merge.
259+
--
260+
-- Only the counter is held. Every other column merges identically for both classes, so a
261+
-- synthetic row still records that the shape was seen, that the field can be null, which
262+
-- published members were exercised, and over what window.
263+
--
245264
-- Values are never stored, only shape -- paths, types, nullability, counts. The single
246265
-- exception is an enum member the vendor's published specification names, because a vendor enum
247266
-- is public data. A string the specification does not name is a customer's data and is
@@ -272,6 +291,7 @@ CREATE TABLE IF NOT EXISTS observed_shape (
272291
-- and `GraphStore.observed_shapes` answers with traffic alone unless asked for everything.
273292
-- `sync.graph.sources` holds which is which and must be extended with this list.
274293
source TEXT NOT NULL,
294+
-- Samples, not writes. What that counts depends on the source class -- see the grain note.
275295
sample_count INTEGER NOT NULL DEFAULT 1,
276296
first_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
277297
last_seen TIMESTAMPTZ NOT NULL DEFAULT now(),

src/sync/graph/sources.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
name appears here and none may, which is what keeps this out of the reach of `CLAUDE.md`'s rule
1313
that vendor knowledge lives in adapters.
1414
15+
The partition decides two things, and the second was added after the first. On read, which rows
16+
a caller asking for traffic receives. On write, whether `sample_count` accumulates: a second
17+
write of a traffic row is a second sample, and a second write of a synthetic row is this ingest
18+
running again over a body Sync built. Both live here because a row's merge and a row's audience
19+
are the same question about the mechanism, and answering them from two lists would let them
20+
disagree.
21+
1522
**Membership is positive.** A source added to `ObservationSource` and forgotten here is then
1623
absent from every baseline rather than silently entering one -- and `ObservedDriftDetector` is
1724
the detector most able to violate precision-over-recall, so the failure that costs recall is the

src/sync/graph/store.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,14 +601,31 @@ def record_observed_shape(self, shape: ObservedShape) -> None:
601601
widens at both ends rather than taking the last write, since sources do not arrive in
602602
order: an error-payload batch can be forwarded hours after a replay run observed the
603603
same shape.
604+
605+
`sample_count` is the one of them whose merge is not idempotent, and it adds only for a
606+
source in `TRAFFIC_SOURCES`. A synthetic row is a body Sync constructed from a published
607+
specification, so writing it again is this ingest running again rather than the shape
608+
being seen again, and a counter that added would measure how often Sync ran against the
609+
floor the detector reads. Held as a maximum rather than by holding the row still: taking
610+
whichever value arrived first would make the counter the only column in this clause that
611+
depends on arrival order, and taking the incoming one would rewrite counts written before
612+
the clause was split.
613+
614+
The classification is read here rather than asserted by the caller. A row's merge is a
615+
property of the mechanism that produced it, and `sync.graph.sources` is where that is
616+
decided for the reader too.
604617
"""
605618
placeholders = ", ".join(["%s"] * len(self._SHAPE_COLUMNS))
606619
self._connect().execute(
607620
f"""
608621
INSERT INTO observed_shape ({", ".join(self._SHAPE_COLUMNS)})
609622
VALUES ({placeholders})
610623
ON CONFLICT (vendor_id, operation_id, field_path, json_type, source) DO UPDATE SET
611-
sample_count = observed_shape.sample_count + EXCLUDED.sample_count,
624+
sample_count = CASE
625+
WHEN observed_shape.source = ANY(%s)
626+
THEN observed_shape.sample_count + EXCLUDED.sample_count
627+
ELSE GREATEST(observed_shape.sample_count, EXCLUDED.sample_count)
628+
END,
612629
nullable_seen = observed_shape.nullable_seen OR EXCLUDED.nullable_seen,
613630
spec_enum_values = ARRAY(
614631
SELECT DISTINCT unnest(observed_shape.spec_enum_values || EXCLUDED.spec_enum_values)
@@ -617,7 +634,10 @@ def record_observed_shape(self, shape: ObservedShape) -> None:
617634
first_seen = LEAST(observed_shape.first_seen, EXCLUDED.first_seen),
618635
last_seen = GREATEST(observed_shape.last_seen, EXCLUDED.last_seen)
619636
""",
620-
[getattr(shape, name) for name in self._SHAPE_COLUMNS],
637+
[
638+
*(getattr(shape, name) for name in self._SHAPE_COLUMNS),
639+
sorted(TRAFFIC_SOURCES),
640+
],
621641
)
622642

623643
def record_observed_call(self, call: ObservedCall) -> None:

tests/test_observed_shape_sources.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Traffic and non-traffic rows are kept apart on read.
1+
"""Traffic and non-traffic rows are kept apart on read, and merged differently on write.
22
33
`observed_shape.source` says which mechanism produced a row. Two consumers read the table as
44
traffic -- `ObservedDriftDetector` and the baseline the mock builder is handed -- and until this
@@ -149,6 +149,98 @@ def test_no_source_is_both_traffic_and_synthetic():
149149
assert TRAFFIC_SOURCES & SYNTHETIC_SOURCES == frozenset()
150150

151151

152+
# --- what the store does with a row it already holds ---------------------------------
153+
#
154+
# The same partition, applied to the conflict clause. `sample_count` is the one column in that
155+
# clause whose merge is not idempotent, and idempotence is what `CLAUDE.md` requires of a stage
156+
# re-run on the same input. For traffic the addition is right and is the reason the column
157+
# exists: two error payloads carrying one shape are two samples. For a synthetic source the
158+
# second write is the same constructed body again, so adding would count how often Sync ran.
159+
#
160+
# Both directions are parametrised over the sets rather than over the values written here, so a
161+
# fourth source added to `sync.graph.sources` arrives with a merge assertion instead of taking
162+
# whichever branch it happens to fall into.
163+
164+
165+
@pytest.mark.parametrize("source", sorted(TRAFFIC_SOURCES))
166+
def test_a_traffic_row_written_again_counts_again(store: GraphStore, source: str):
167+
"""The counterpart that keeps the synthetic assertion below from reading as a frozen
168+
counter. A clause that stopped counting for every source would satisfy that test and
169+
destroy the sample floor, which is the failure mode this pair exists to separate.
170+
"""
171+
for _ in range(3):
172+
store.record_observed_shape(_shape(source=source))
173+
174+
rows = store.observed_shapes("stripe", "PostCharges", traffic_only=False)
175+
assert [row.sample_count for row in rows] == [3]
176+
177+
178+
@pytest.mark.parametrize("source", sorted(SYNTHETIC_SOURCES))
179+
def test_a_synthetic_row_written_again_does_not_count_again(store: GraphStore, source: str):
180+
"""A synthetic row is a body Sync constructed, so writing it a second time is the ingest
181+
running a second time and not the shape being seen a second time. The count is one sample
182+
however many times the write happens.
183+
"""
184+
for _ in range(3):
185+
store.record_observed_shape(_shape(source=source))
186+
187+
rows = store.observed_shapes("stripe", "PostCharges", traffic_only=False)
188+
assert [row.sample_count for row in rows] == [1]
189+
190+
191+
def test_a_synthetic_count_written_before_this_clause_is_not_rewritten(store: GraphStore):
192+
"""Rows already in a database were written under a clause that added, so some hold counts
193+
above one. Taking the incoming value would rewrite that history on the next write, which is
194+
a migration performed silently by a merge rather than a merge holding a counter still.
195+
"""
196+
store.record_observed_shape(_shape(source="replay", sample_count=5))
197+
store.record_observed_shape(_shape(source="replay", sample_count=1))
198+
199+
rows = store.observed_shapes("stripe", "PostCharges", traffic_only=False)
200+
assert [row.sample_count for row in rows] == [5]
201+
202+
203+
def test_a_synthetic_rows_count_does_not_depend_on_arrival_order(store: GraphStore):
204+
"""Every other column in this clause merges the same way whichever write lands first --
205+
`LEAST`, `GREATEST`, `OR`, a union -- because sources do not arrive in order. Keeping
206+
whatever the row already held would make the counter the one column that reads the
207+
sequence, which is the property `test_an_observation_arriving_out_of_order_does_not_rewind
208+
_the_window` already refuses for the timestamps.
209+
"""
210+
store.record_observed_shape(_shape(source="replay", field_path="/a", sample_count=5))
211+
store.record_observed_shape(_shape(source="replay", field_path="/a", sample_count=1))
212+
store.record_observed_shape(_shape(source="replay", field_path="/b", sample_count=1))
213+
store.record_observed_shape(_shape(source="replay", field_path="/b", sample_count=5))
214+
215+
rows = store.observed_shapes("stripe", "PostCharges", traffic_only=False)
216+
assert {row.field_path: row.sample_count for row in rows} == {"/a": 5, "/b": 5}
217+
218+
219+
def test_a_synthetic_row_written_again_still_gains_evidence_and_widens_its_window(
220+
store: GraphStore,
221+
):
222+
"""Only the counter is held. `DO NOTHING` for synthetic sources would converge too, and
223+
would throw away the rest of the merge: a later write proving the field can be null, or an
224+
enum member an earlier specification did not name, or the window this row covers. The row
225+
still records that the shape was seen and when -- it stops recording how many times the
226+
write happened.
227+
"""
228+
later = NOW + timedelta(days=1)
229+
store.record_observed_shape(_shape(source="replay", nullable_seen=False))
230+
store.record_observed_shape(
231+
_shape(
232+
source="replay", nullable_seen=True, spec_enum_values=["succeeded"],
233+
first_seen=EARLIER, last_seen=later,
234+
)
235+
)
236+
237+
row = store.observed_shapes("stripe", "PostCharges", traffic_only=False)[0]
238+
assert row.sample_count == 1
239+
assert row.nullable_seen is True
240+
assert row.spec_enum_values == ["succeeded"]
241+
assert (row.first_seen, row.last_seen) == (EARLIER, later)
242+
243+
152244
# --- the two consumers, end to end --------------------------------------------------
153245

154246

@@ -287,6 +379,24 @@ def test_rows_written_before_the_filter_existed_survive_a_second_apply_schema(st
287379
]
288380

289381

382+
def test_a_held_synthetic_count_survives_a_second_apply_schema_and_a_further_write(
383+
store: GraphStore,
384+
):
385+
"""The same assertion for the clause rather than for the column list, because the clause is
386+
what this task changed. A `replay` row at 5 was written under a clause that added, and it
387+
has to come through both a re-applied schema and a further write intact -- neither reset to
388+
1 nor advanced to 6.
389+
"""
390+
store.record_observed_shape(_shape(source="replay", sample_count=5))
391+
392+
store.apply_schema()
393+
store.apply_schema()
394+
store.record_observed_shape(_shape(source="replay"))
395+
396+
rows = store.observed_shapes("stripe", "PostCharges", traffic_only=False)
397+
assert [row.sample_count for row in rows] == [5]
398+
399+
290400
# --- the defect this task did not fix, fixed by M3-W122 -----------------------------
291401

292402

0 commit comments

Comments
 (0)