Skip to content

parity: classify excluded Swift role twins - #2577

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
kavemang:fix/parity-out-of-scope-twins-2567
Sep 29, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
kavemang:fix/parity-out-of-scope-twins-2567

Conversation

@kavemang

@kavemang kavemang commented Sep 28, 2026 •

Copy link
Copy Markdown

Summary

  • Add a typed out_of_scope_twin disposition for Kotlin declarations whose Swift role twin lives in the excluded Strand/ app tree.
  • Validate that the Swift path is present and narrowly allow inherited platform_specific entries to be reclassified without changing their debt identity.
  • Correct four existing dispositions and document that these links do not prove behavioral or byte parity.

Refs #2567.

Verification

  • python3 -m unittest Tools.tests.test_parity_disposition_kinds (4 passed)
  • python3 -m unittest Tools.tests.test_parity_governance_acceptance (50 passed)
  • python3 Tools/parity_ratchet.py --base upstream/main --offline (0 errors)
  • git diff --check (passed)

ryanbr added a commit that referenced this pull request Sep 29, 2026
`Tools/tests/test_parity_governance_acceptance.py` does not test the scanner. It
asserts that the CHECKED-IN authority and baseline still reproduce from the
CURRENT product source, so a product change is what invalidates it. It ran only
in parity-governance.yml, whose filter names `Tools/parity_*`.

So a change could move the declaration and call-site graph the ledger measures
without the gate that measures it ever running.

That is not hypothetical. Earlier today #2557 took the only production call to
`primarySessionRestingHR` on both platforms, turning it test-only, and the two
resulting findings went unrecorded. Seventeen green checks, none of them this
one. `main` carried a red ratchet until #2581, and it surfaced on #2577, an
outside contributor's PR that had caused none of it. parity-governance.yml's own
header notes that shape had already happened four times, three of them on an
outside contributor.

Here, and not behind a filter naming product paths, for the reason the guard test
below already gives about a different suite: the list would be whatever the
assertions happen to read today, so a filter only moves the trap. #1691 is the
precedent, where a `Tools/**` filter silenced a suite that reads
`TodayScreen.kt` and `AppModel.swift`, and main carried it red through nine
merges.

Its own step and its own floor, so the core floor of 114 stays independent, which
is the reasoning already written beside that floor.

Cost, measured on this runner rather than estimated: 51 s for 49 tests, against
roughly 25 s for the rest of the leg. I first assumed this suite cost 4 to 6
minutes and designed a separate parallel workflow around that; the 4 to 6 minutes
is `parity_ratchet.py`, which checks out the base tree and derives twice. This
suite derives once.

The guard is extended to assert the unfiltered leg actually invokes the module.
The failure mode is not someone arguing against this, it is someone dropping the
step or hanging a filter off it later, which is exactly what #1691 was.
Mutation-verified: pointing the step at a different module makes the guard fail.
The assertion reads the dotted module path, which appears only in the invocation
and not in the prose above it, so a comment cannot satisfy it.

Verified: `tests.test_parity_governance_acceptance` plus
`tests.test_parity_disposition_kinds`, 51 tests, pass. The workflow parses.

Refs #2567, #2557, #1691
ryanbr added a commit that referenced this pull request Sep 29, 2026
`Tools/tests/test_parity_governance_acceptance.py` does not test the scanner. It
asserts that the CHECKED-IN authority and baseline still reproduce from the
CURRENT product source, so a product change is what invalidates it. It ran only
in parity-governance.yml, whose filter names `Tools/parity_*`.

So a change could move the declaration and call-site graph the ledger measures
without the gate that measures it ever running.

That is not hypothetical. Earlier today #2557 took the only production call to
`primarySessionRestingHR` on both platforms, turning it test-only, and the two
resulting findings went unrecorded. Seventeen green checks, none of them this
one. `main` carried a red ratchet until #2581, and it surfaced on #2577, an
outside contributor's PR that had caused none of it. parity-governance.yml's own
header notes that shape had already happened four times, three of them on an
outside contributor.

Here, and not behind a filter naming product paths, for the reason the guard test
below already gives about a different suite: the list would be whatever the
assertions happen to read today, so a filter only moves the trap. #1691 is the
precedent, where a `Tools/**` filter silenced a suite that reads
`TodayScreen.kt` and `AppModel.swift`, and main carried it red through nine
merges.

Its own step and its own floor, so the core floor of 114 stays independent, which
is the reasoning already written beside that floor.

Cost, measured on this runner rather than estimated: 51 s for 49 tests, against
roughly 25 s for the rest of the leg. I first assumed this suite cost 4 to 6
minutes and designed a separate parallel workflow around that; the 4 to 6 minutes
is `parity_ratchet.py`, which checks out the base tree and derives twice. This
suite derives once.

The guard is extended to assert the unfiltered leg actually invokes the module.
The failure mode is not someone arguing against this, it is someone dropping the
step or hanging a filter off it later, which is exactly what #1691 was.
Mutation-verified: pointing the step at a different module makes the guard fail.
The assertion reads the dotted module path, which appears only in the invocation
and not in the prose above it, so a comment cannot satisfy it.

Verified: `tests.test_parity_governance_acceptance` plus
`tests.test_parity_disposition_kinds`, 51 tests, pass. The workflow parses.

Refs #2567, #2557, #1691
@ryanbr

ryanbr commented Sep 29, 2026

Copy link
Copy Markdown
Owner

@kavemang Two things, and the first one is not about your change.

This PR was never broken. While reviewing it I rebased it onto main and the ratchet failed with two errors about test-only-callsite groups. They were not yours. main itself was red at the time, and I had made it red about an hour earlier by merging #2557.

#2557 took the daily resting HR from the primary night's five-minute floor, which replaced the only production call to primarySessionRestingHR on both platforms. The function became test-only, two findings appeared, and nothing recorded them. #2557 touched no Tools/parity_* path, so parity-governance never ran on it: seventeen green checks and not one of them the gate that governs call-site structure.

That is the shape parity-governance.yml's own header describes, where a later PR touching Tools/ inherits drift it did not cause. Its header notes it had happened four times before, three of them on an outside contributor. This is the next one and it landed on you. Sorry about that.

#2581 recorded the two findings and main is green again. #2582 then moved the repository-acceptance suite onto the unfiltered leg so a change that moves the declaration and call-site graph can no longer do it without the gate running. Both are merged, so a rebase here should be clean.

On the PR itself. It is good work, and I say that with an interest to declare: it implements a remedy I proposed in #2567, so I went looking for the failure mode where a nicer type name records the same unchecked assertion. It does not.

twin_path is mandatory, regex-pinned to the excluded Strand/ tree, restricted to Kotlin declarations, and the file existence is genuinely checked rather than the field merely being non-empty. Passing root only for the current registry and not for the base is the right call: a file that moved since the base should not fail history.

_is_out_of_scope_reclassification pinning kind, identity, identity_sha256 and platform is the part that makes the reclassification safe rather than a hole, and the three tests cover exactly the two risky properties plus the inherited path.

I also checked the four corrected entries by hand, because the gate validates the file and not the symbol. All three paths exist; computeHabitualSleep and hrFingerprintUnion are real. finishedSessions honestly says the Swift side is inline, so there is nothing to name. And the hrUnionFingerprint rationale now points at Repository.hrFingerprintUnion rather than the caller it used to name, which corrects a factual error I had flagged in #2567.

One note, not blocking. The issue-binding skip went from an allowlist to a denylist:

-  if exemption["type"] == "platform_specific": continue
+  if exemption["type"] != "experimental": continue

A fourth type added later inherits "no issue required" by default rather than by decision. It is not reachable today, since _validate_dispositions rejects unknown types before this loop, but it couples two places about 460 lines apart, which is the shape StreamReadCap exists to complain about. Worth a comment there rather than a change.

Please rebase and I will take it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants