From d26c6a49a9be8377371d2af2de36c5118164ab8d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 29 Oct 2026 18:07:36 -0400 Subject: [PATCH 1/2] Compare a stored record's required keys as a set UncarriedRecordFormat compared StoredForm.required as an ordered list, so declaring a field in a parent class (which reorders the schema's required list without changing what is written) counted as an uncarried format change. #3607 hit it on Thread. The required keys are a set: StoredForm owns that, and both revisions' probe output is read through from_wire. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK --- tools/debt_ratchet.py | 2 +- tools/stored_formats.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/tools/debt_ratchet.py b/tools/debt_ratchet.py index 301c03804a..1e7ad62cda 100644 --- a/tools/debt_ratchet.py +++ b/tools/debt_ratchet.py @@ -2658,7 +2658,7 @@ def measured_formats(cls, repo: Path, ref: str, root: str, ) if result.returncode: raise ValueError(f"{cls.__name__} could not measure {ref}: {result.stderr.decode()}") - return {name: StoredForm(**form) for name, form in json.loads(result.stdout).items()} + return {name: StoredForm.from_wire(form) for name, form in json.loads(result.stdout).items()} @classmethod def counted(cls, repo: Path, base: str, head: str, changed: set[str], root: str): diff --git a/tools/stored_formats.py b/tools/stored_formats.py index cda05db30d..9ce76e619d 100644 --- a/tools/stored_formats.py +++ b/tools/stored_formats.py @@ -25,7 +25,9 @@ class StoredForm: version: int fields: dict[object, object] - required: list[str] + required: frozenset[str] + """The keys every record carries: a set, because declaring a field in a parent class + reorders the schema's list without changing what is written.""" @classmethod def of(cls, record: type) -> StoredForm | None: @@ -34,11 +36,18 @@ def of(cls, record: type) -> StoredForm | None: declared = vars(record).get("version") match FieldCodec.record_schema(record): case {"properties": {"version": {"enum": [stamped]}, **stored}, "required": required}: - return cls(stamped, stored, [key for key in required if key in stored]) + return cls(stamped, stored, frozenset(key for key in required if key in stored)) case {"properties": properties, "required": required} if isinstance(declared, int): - return cls(declared, properties, required) + return cls(declared, properties, frozenset(required)) return None + @classmethod + def from_wire(cls, form: dict) -> StoredForm: + return cls(form["version"], form["fields"], frozenset(form["required"])) + + def wire(self) -> dict[str, object]: + return {"version": self.version, "fields": self.fields, "required": sorted(self.required)} + def stored_formats(module_name: str) -> dict[str, StoredForm]: module = importlib.import_module(module_name) @@ -59,7 +68,7 @@ def main(modules: list[str]) -> None: print( json.dumps( { - name: dataclasses.asdict(form) + name: form.wire() for module in modules for name, form in stored_formats(module).items() } From 93ffe78cd6a614355fc25afc25e674d10840433c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 29 Oct 2026 18:26:33 -0400 Subject: [PATCH 2/2] Let StoredForm own its required-key set at construction The previous head read the probe's JSON through from_wire with string subscripts, which the ratchet rightly counted. StoredForm's __post_init__ now turns whatever sequence it is given into the frozenset, the probe serializes it sorted, and the ratchet constructs it as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK --- tools/debt_ratchet.py | 2 +- tools/stored_formats.py | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/tools/debt_ratchet.py b/tools/debt_ratchet.py index 1e7ad62cda..301c03804a 100644 --- a/tools/debt_ratchet.py +++ b/tools/debt_ratchet.py @@ -2658,7 +2658,7 @@ def measured_formats(cls, repo: Path, ref: str, root: str, ) if result.returncode: raise ValueError(f"{cls.__name__} could not measure {ref}: {result.stderr.decode()}") - return {name: StoredForm.from_wire(form) for name, form in json.loads(result.stdout).items()} + return {name: StoredForm(**form) for name, form in json.loads(result.stdout).items()} @classmethod def counted(cls, repo: Path, base: str, head: str, changed: set[str], root: str): diff --git a/tools/stored_formats.py b/tools/stored_formats.py index 9ce76e619d..5281406e78 100644 --- a/tools/stored_formats.py +++ b/tools/stored_formats.py @@ -36,17 +36,14 @@ def of(cls, record: type) -> StoredForm | None: declared = vars(record).get("version") match FieldCodec.record_schema(record): case {"properties": {"version": {"enum": [stamped]}, **stored}, "required": required}: - return cls(stamped, stored, frozenset(key for key in required if key in stored)) + return cls(stamped, stored, [key for key in required if key in stored]) case {"properties": properties, "required": required} if isinstance(declared, int): - return cls(declared, properties, frozenset(required)) + return cls(declared, properties, required) return None - @classmethod - def from_wire(cls, form: dict) -> StoredForm: - return cls(form["version"], form["fields"], frozenset(form["required"])) - - def wire(self) -> dict[str, object]: - return {"version": self.version, "fields": self.fields, "required": sorted(self.required)} + def __post_init__(self) -> None: + """Each revision's probe prints the keys as a JSON list; their order means nothing.""" + object.__setattr__(self, "required", frozenset(self.required)) def stored_formats(module_name: str) -> dict[str, StoredForm]: @@ -68,9 +65,10 @@ def main(modules: list[str]) -> None: print( json.dumps( { - name: form.wire() + name: dataclasses.asdict(form) for module in modules for name, form in stored_formats(module).items() - } + }, + default=sorted, ) )