Repository navigation
refactor(data): one HR union fingerprint, not two that were free to disagree - #2570
Merged
Merged
Conversation
…isagree WhoopRepository carried two functions computing the same value. Issue #2566. hrUnionFingerprint/3 line 1185, from #2293 hrFingerprintWindowUnion/3 line 728, which I added in #2543 Same signature, same rawWhoopSourceIds walk, same COUNT plus MAX per id. They differed only in how they punctuated the result: "id=count:maxTs" joined with a comma against "id:count:maxTs" joined with a pipe. Neither encoding is observable. StressLensCache is an in-memory object holding a single key, and PhysiologicalStepCycleEngine's loadCache is a plain HashMap field of a private data class. Nothing persists either string or sends it anywhere, so both doc comments were right that the format is free to change. The redundancy was not the risk. Two witnesses of one fact that are allowed to drift is the risk, and the fact they witness is whether cached scoring work can be reused. Teach one union to walk an extra id or carry a third aggregate and the other keeps answering the older question, silently. So: hrFingerprintWindowUnion is gone, the daytime stress lens memo reads hrUnionFingerprint, and hrUnionFingerprint now calls hrFingerprintWindow for each id rather than repeating the two DAO reads inline. One union, one encoding, one per-id read. The cycle load cache keeps the exact value it had. Strand/Data/Repository.swift referenced the deleted function as its Kotlin twin, so that pointer moves to the survivor. Both sides now also say what kind of twin this is: each compares its own value against its own previous value, neither is persisted or transmitted, and the two encode the same facts differently. There is no byte-identity contract here and no oracle asserting one. Without that note the next reader could take "twin" for the usual byte-level pair this repository means by the word and align the encodings believing they were repairing something. Neither union had a single test, which is how a duplicate survived review. HrUnionFingerprintTest pins the property that motivated the union at all: a row landing under an alias id or under the canonical id must move the witness even when the active id is untouched (#908). A fingerprint keyed on the active id alone passes a naive equality check and fails these. The stubbed DAO throws on any method other than the two index aggregates, so a row fetch creeping into the cheap path fails too. Mutation-verified rather than assumed. Replacing the union walk with the bare active id reddens 4 of 6; dropping maxTs from each entry reddens a different 4 of 6. The survivors are explainable in both cases: stableWhenNothingChanged only asserts determinism, and canonicalActiveIsNotDuplicated sets the active id to the canonical one, where the union and the single id genuinely coincide.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue #2566.
WhoopRepositorycarried two functions computing the same value.hrUnionFingerprint/3(line 1185, #2293)hrFingerprintWindowUnion/3(line 728, #2543)(activeDeviceId: String, from: Long, to: Long): StringrawWhoopSourceIds(activeDeviceId)dao.countHrInWindow+dao.maxHrTsInWindowinlinehrFingerprintWindow"$id=$count:$maxTs""$id:$count:$maxTs",|I added the second one in #2543 without noticing the first.
Why the encodings were free to change
Neither string is observable.
StressLensCacheis an in-memoryobjectholding one key, andPhysiologicalStepCycleEngine.loadCacheis a plainHashMapfield of a private data class. Nothing persists either value or sends it anywhere, so both doc comments were correct that the format is free, and switching a caller from one encoding to the other costs nothing beyond a cold cache at process start, which it always has.Why it was worth removing anyway
The redundancy was not the risk. Two witnesses of one fact that are allowed to drift is the risk, and the fact they witness is whether cached scoring work can be reused. Teach one union to walk an extra id, or to carry a third aggregate, and the other keeps answering the older question silently.
The change
hrFingerprintWindowUnionis gone. The daytime stress lens memo readshrUnionFingerprint, which now callshrFingerprintWindowper id rather than repeating the two DAO reads inline. One union, one encoding, one per-id read. The cycle load cache keeps the exact value it had, since the survivor is the one it already used.Strand/Data/Repository.swiftnamed the deleted function as its Kotlin twin, so that pointer moves to the survivor. Both sides now also say what KIND of twin this is: each compares its own value against its own previous value, neither is persisted or transmitted, and the two encode the same facts differently. There is no byte-identity contract and no oracle asserting one. Without that note the next reader could take "twin" for the byte-level pair this repository usually means by the word, and align the encodings believing they were repairing something.Tests
Neither union had a single test, which is how a duplicate survived review in the first place.
HrUnionFingerprintTestpins the property that motivated a union at all (#908): a row landing under an ALIAS id, or under the canonical id, must move the witness although the active id is untouched. A fingerprint keyed on the active id alone passes a naive equality check and fails these. The stubbed DAO throws on any method other than the two index aggregates, so a row fetch creeping into the cheap path fails too.Mutation-verified rather than assumed, with both mutations chosen to disable the guard rather than merely perturb it:
maxTsfrom each entrywalksEveryUnionIdNotJustTheActivemovesWhenARowLandsUnderTheCanonicalIdOnlyemptyWindowIsStableZeroesmovesWhenARowLandsUnderAnAliasIdOnlycanonicalActiveIsNotDuplicatedstableWhenNothingChangedDifferent kill sets, so the tests discriminate distinct properties rather than all keying on one thing. Every survivor is explainable:
movesWhenARowLandsUnderAnAliasIdOnlyalso moves the count, so droppingmaxTsstill registers a change;canonicalActiveIsNotDuplicatedsets the active id TO the canonical one, where the union and the single id genuinely coincide;stableWhenNothingChangedasserts determinism only, which both mutants preserve.Verification
parity_ratchet.py --base origin/main:errors=0, no warnings.functions4508 to 4507 andunpaired_functions4109 to 4108, nothing else. Debt decreases by one, so no disposition is required.doc_comment_lint.py: green.Note for #2567: neither of these functions can pair, because
Strand/Data/Repository.swiftis outside the ledger's Swift scope. That blind spot is how two Kotlin functions computing the same value both survived review, with nothing able to notice they were twins of each other.