Skip to content

feat: contribute 1.7.13 -> 1.7.14 (kinematic speed gate against FMDN teleport fixes, round-trip recovery, manual-locate log redaction, branch-neutral release automation)#205

Merged
jleinenbach merged 13 commits into
BSkando:1.7from
jleinenbach:1.7
Jul 18, 2026

Conversation

@jleinenbach

Copy link
Copy Markdown
Collaborator

Summary

This contributes the next slice of the jleinenbach fork's 1.7 line (integration 1.7.13 -> 1.7.14) on top of the already-merged 1.7.13 contribution (#204). It bundles 6 fork PRs plus one automated release commit: a headline kinematic speed gate that rejects physically impossible crowd-sourced "teleport" fixes, a stateful round-trip recovery so a legitimate there-and-back trip is not lost to that gate, a manual-locate log privacy fix, and a branch-neutral release automation for the release/version-stamp toolchain.

GitHub shows a clean 7-commit (plus one catch-up merge), 34-file diff (+2986 / -97). That line count is misleading: the runtime code change is small (custom_components/**/*.py = 7 files, +432 / -53). The bulk is new tests (tests/** = 9 files, +1999 / -3) and the release/CI toolchain (.github/** + pyproject.toml + script/** = 4 files, +497 / -33); the i18n/package strings are 12 files, +46 / -0 (two new option labels across ten locales). Nothing shipped to Home Assistant users depends on the CI toolchain.

This becomes the BSkando 1.7.14 release; the tag/release itself is a separate next step. The version literal already present in the code (const.py / manifest.json / pyproject.toml) is 1.7.14.0, the four-part PEP 440 form the new release automation produces (see the reviewer note under Changed).

  • Type: feat + fix + test + build (release/CI toolchain)
  • Scope/Area: crowd-fix location validation (speed gate + round-trip recovery), manual-locate diagnostic log redaction, release/version-stamp automation, test coverage
  • Linked fork PRs: #1193, #1194, #1195, #1197, #1198, #1199

Note for reviewers: the two 1.7 branches share a merge base (the merged #204 / 1.7.13 tip), so GitHub shows a clean diff, not a full tree diff. The range is 7 functional commits plus one catch-up merge (Merge branch 'BSkando:1.7' into 1.7) that only pulls this base forward into the fork; the fork is 0 commits behind the base. There are no reverts and no user-facing file deletions in this range. Every bundled fork PR was individually reviewed (Codex) and CI-green on the fork before merge; the speed-gate/round-trip anchor logic in particular went through several Codex correctness iterations (#1195 -> #1196 -> #1197 -> #1198).


Motivation

Since 1.7.13, the crowd-sourced location path needed hardening against the Find My Device Network's "teleport" failure mode, plus two smaller fixes worth contributing together. In plain terms:

  1. The tracker occasionally jumped hundreds of kilometres for a moment and then snapped back. In the FMDN crowd network the published point is the GPS position of the finder's phone, not the tracker; a stale or mis-timed finder location writes the tracker to the wrong place at physically impossible speed. (Discussion Minimum accuracy/score filter #177, #1195)
  2. But a genuine there-and-back trip must not be thrown away with the bad jumps. A naive speed filter would also reject a legitimate quick return. (#1197, #1198)
  3. The manual-locate diagnostic log printed the device name and a misleading cause list. (#1193, #1194)
  4. The fork needed a reproducible, branch-neutral way to cut releases and keep the three version files in sync. (#1199)

Change Log (human-readable)

Added

  • Kinematic speed gate against impossible "teleport" fixes (#1195, behavioural -- headline). A new default-on option speed_gate_enabled ("Reject impossible location jumps"). When a crowd fix would require a physically impossible point-to-point speed to be reached from the previous accepted position, it is rejected instead of teleporting the tracker. The gate is an absolute physical cap (DEFAULT_MAX_PLAUSIBLE_SPEED_MPS = 400.0, ~1440 km/h), not relative to your usual speed, so a real flight still passes. It is accuracy-blind (it looks at the jump, not the reported accuracy) and your own GPS reports always bypass it. Users can disable it if a legitimate fast movement is ever dropped. A new speed_gate_enabled label/description is added to all ten locales.
  • Stateful round-trip recovery (#1197, #1198, behavioural). A new default-on option roundtrip_confirm_enabled ("Recover legitimate return trips"). When a long jump A -> B is accepted, the pre-jump position A is anchored in memory (radius ROUND_TRIP_ANCHOR_RADIUS_M = 200 m, TTL ~15 min). A later crowd fix that the forward speed gate would otherwise reject as a teleport is accepted instead if it lands back near A within the TTL -- recovering a genuine return trip rather than stranding the tracker at the far position. It only ever applies where the speed gate would otherwise reject, so it can never loosen validation for normal fixes. A new roundtrip_confirm_enabled label/description is added to all ten locales.

Fixed

  • Redacted the device name across the remaining manual-locate log sites and dropped a misleading cause list (#1193, #1194, privacy/clarity). The manual-locate diagnostic path still emitted the raw device name at several log sites and printed a cause list that could point at the wrong reason. The name is now redacted consistently and the misleading list is removed, closing the R6 redaction sweep for this path.

Changed

  • Branch-neutral semantic-release with three-file version sync and a release-stamp workflow (#1199, dev/CI only). The release automation is reworked so it runs regardless of which fork/branch cuts the release (branch-neutral), keeps the three version literals in lockstep (const.py / manifest.json / pyproject.toml, an invariant anchored in custom_components/googlefindmy/AGENTS.md), and stamps the published artifact. This is where most of the non-test line count lives (release.yml, new release-stamp.yml, new script/stamp_version.py, pyproject.toml PSR config). Reviewer note on the version literal: the automation emits a four-part PEP 440 version (1.7.14.0); the on-disk files therefore read 1.7.14.0 rather than 1.7.14. Branch and shipped HACS asset always carry the stamped value. The automation is branch-neutral by design so upstream can adopt it, but it is dev/CI only -- BSkando can keep or drop the two workflow files independently; nothing shipped to Home Assistant users depends on them.

Removed

  • Nothing user-facing. No files are deleted in this range.

Compatibility

  • No breaking changes expected. The config_entry schema version is unchanged (CONFIG_ENTRY_VERSION = 2); existing installs are unaffected. Both new options default to on and preserve prior behaviour for normal fixes (the speed gate only rejects physically impossible jumps; the round-trip recovery only accepts fixes the gate would otherwise reject). The round-trip anchor is RAM-only (it does not survive a restart) -- intentional, cheap, and self-healing.

Security/Privacy

  • The manual-locate diagnostic no longer leaks the device name to logs (#1193, #1194). No changes to key handling or stored secret material. The speed gate and round-trip recovery operate purely on already-received location fixes and add no new data collection.

Tests

  • New/extended contract tests (tests-only): test_cache_speed_gate.py (+447), test_cache_round_trip.py (+816), test_location_request_r6_name_sweep.py (+373), test_stamp_version.py (+220), test_hacs_validation.py (+56), plus small additions to coordinator/plus-code/hypothesis/surfacing tests.
  • Key regressions are mutation-sharp (they turn red when the fix is reverted): the absolute-cap speed gate (own-report bypass, accuracy-blind, flight passes, teleport rejected); the round-trip anchor lifecycle (seed only from a reliably-accepted gated fix, symmetric seed/consume provenance, TTL keyed to acceptance time, shared-device correctness, out-of-order handling); and the R6 name-redaction sweep across every manual-locate log site.
  • Provenance note (honest): this PR bundles already-merged, individually CI-green and Codex-reviewed fork commits; no new code was added in this bundling step. The anchor-lifecycle correctness in particular was hardened across #1195 -> #1198 in response to successive Codex findings.

Risk & Rollback

  • Risk level: low to moderate. The runtime footprint is contained (custom_components/**/*.py = 7 files, +432 / -53), the two behavioural changes are default-on but conservative (reject only the physically impossible, recover only the otherwise-rejected), and there is no schema change or data migration. The main new state is the RAM-only round-trip anchor.
  • Rollback: both behaviours are individually switchable off via the new options; a full revert of the commit range restores prior behaviour. There are no reverts, no user-facing file deletions and no config-entry schema change to unwind.

jleinenbach and others added 8 commits July 15, 2026 21:41
…l-locate diagnostic (#1193)

* fix(nova): redact device name and drop misleading cause list in manual-locate diagnostic

The `not fcm_token` branch in `get_location_data_for_device` logged
"Failed to get FCM token for %s: ... (client/token unavailable or no
coordinator)" at ERROR with the raw device name. This had two defects:

1. R6 redaction (AGENTS.md Section 5): the raw device display name
   (e.g. "samsung SM-S938B") leaked into a user-facing ERROR record.
   The rest of this module already keeps such names at DEBUG only
   (see the outer surfacing handler in the same function).

2. Misleading diagnostics: the hard-coded closed set of three causes
   was incomplete. `async_register_for_location_updates` fails closed
   on several paths (missing canonical id, no coordinator, unavailable
   client/token, and a fail-closed reconnect that never reached
   STARTED), and each already logs its own specific WARNING immediately
   before returning falsy. When the real cause was the reconnect/STARTED
   timeout, the enumerated list pointed readers at the wrong thing.

The branch now defers to that authoritative preceding WARNING, drops the
misleading enumeration, keeps the device name at DEBUG only, and reports
at WARNING instead of ERROR (the outcome is fail-closed and self-heals on
the next poll, so a second ERROR overstated the severity).

Adds a regression test that fails on the old behavior (name at ERROR,
enumeration present) and passes with the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(test): apply ruff format to regression test

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…s (R6) (#1194)

Completes the R6 redaction started in PR #1193. Every remaining user-facing
(>= WARNING) record in `location_request.py` logged the raw device display
name via a `... for %s` argument, in violation of R6 (AGENTS.md Section 5:
redact derived identifiers such as device names from logs). Each such record
now states the failure and its cause without the name and keeps the raw name
at DEBUG only, mirroring the module's established Count@WARNING / Name@DEBUG
pattern (PR #1129 / #1193).

Swept sites (16):

`get_location_data_for_device`
- FCM registration failure (ERROR)
- Nova request ladder: rate-limit, server/HTTP, auth, network/client, generic
  (WARNING/WARNING/WARNING/WARNING/ERROR)
- unexpected-device mismatch (WARNING)
- FCM unregister failure in `finally` (WARNING)

`_make_location_callback`
- decoder/decrypt import and attribute failures (ERROR)
- protobuf parse failure (ERROR)
- per-device decrypt failure (ERROR for session/auth, WARNING for per-device)
- unexpected async-decryption error (ERROR)
- empty-after-decrypt (WARNING)
- outer callback failure (ERROR)

The two sites beyond PR #1193's enumerated follow-up list (NovaHTTPError server
error, unexpected-device mismatch) are included because they are the same R6
class; a partial sweep would leave the class inconsistent.

Visible message prefixes are preserved so existing severity/diagnostic tests
(e.g. the "Failed to process location data" filter) keep matching. Adds a
dedicated regression suite that is RED on the pre-fix code (sentinel device
name present in a >= WARNING record) and GREEN with the redaction, covering
both the caller ladder and the callback branches.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Add kinematic speed gate to reject FMDN teleport fixes (#177)

* i18n: sync speed_gate_enabled option into all locales

The translations validation gate (sync_translations.py --check) requires the
new speed_gate_enabled option key to be present in every locale. Populate the
eight remaining locales (es, fr, he, it, nl, pl, pt-BR, pt) via the canonical
script/sync_translations.py, which seeds the English source text as a
fallback until translated.

* fix(cache): let accuracy-less crowd fixes bypass the speed gate

The kinematic speed gate hard-dropped a far crowd fix via return False before
the downstream _is_significant_update sanitization ran. For an accuracy-less
fix (accuracy=None) that sanitization is what flags accuracy_estimated=True, so
dropping it regressed the last-good protection covered by
test_plus_code_last_known.py (KeyError: 'accuracy_estimated').

An accuracy-less crowd report is not a clean teleport discriminant anyway: the
decoder strips the accuracy number (acc_rank=-inf) and the reliable last-good is
already protected by is_reliable_fix, independent of this gate. Guard the gate
with 'new_acc_raw is not None' so accuracy-less fixes fall through. Add a
mutation-guard test (accuracy-less crowd fix at impossible speed -> passes, no
reject counter). Real crowd teleports carry a measured accuracy and stay gated.

* fix(cache): treat invalid accuracies as accuracy-less before speed gating

Codex review of 5bfeafd: the #1181 bypass guarded only the missing-key
case (new_acc_raw is None). A crowd fix carrying Android's no-accuracy
sentinel (accuracy=0.0) or any sub-physical/non-finite value coerces to a
non-None float, so the gate could still hard-drop it via `return False`
before _is_significant_update sanitizes it into accuracy_estimated=True,
regressing the #1181 last-good protection for that class of report.

Widen the guard to the same error-code set that _safe_accuracy() maps to
the 200m fallback: gate only a fix whose accuracy is present, finite and
>= MIN_PHYSICAL_ACCURACY_M. Reuses the already-imported math and
MIN_PHYSICAL_ACCURACY_M SSOT; no new private helper, no status_code
duplication.

Add regression tests 21-22 pinning the sentinel (0.0) and sub-physical
(0.0005) cases fall through the gate and leave the reject counter untouched.

* fix(cache): key own-report gate bypass off cryptographic provenance

Codex review of 74600c2: the own-report bypass read the raw
`is_own_report` flag. A network/foreign report can carry a spurious
server-supplied `is_own_report=True` - this integration's own crowdsourced
uploader stamps network reports that way while they carry a non-empty
publicKeyRandom (decrypt_locations.py:1991-1998). Keying the bypass off the
raw flag alone would let such a spoofed own-flag skip the speed gate, so an
impossible teleport from that crowd report would still be accepted.

The decrypt layer already hardens is_own_report = is_own_report and not
is_network_report (decrypted_location.py:39). Mirror that invariant at the
gate so the bypass uses cryptographic provenance on every path: bypass only
when is_own_report is set AND is_network_report is falsy. An absent
is_network_report is treated as not-network (genuine own reports never carry
the network flag), so real own fixes keep bypassing.

Add regression tests 23-24: a spoofed network report (both flags set) at
teleport speed is now gated and counted, while a genuine own report with no
network flag still bypasses.
…1197)

Net content delta of the round-trip escape hatch (#1196) rebuilt as a
single commit on top of 1.7. The kinematic speed gate (#1195) is already
in 1.7 via squash-merge, so its content nets to zero here; this commit
carries only the round-trip additions (+538/-0, 19 files).

Rebuild avoids the squash-merge divergence that made a direct merge of
feature/speed-gate-teleport conflict against 1.7.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…1196 follow-up) (#1198)

* fix(cache): round-trip anchor lifecycle + symmetric speed-gate guard (#1196 follow-up)

Three coherent fixes in the round-trip / speed-gate region of cache.py,
all against the 1.7 baseline that now carries the stateful round-trip
escape hatch (#1197):

- F-FABLE-1: guard the anchor housekeeping pop on ``delta_anchor >= 0``.
  A delayed out-of-order report with last_seen between anchor A and B
  yields delta_anchor < 0; popping there would destroy the anchor and
  strand a later genuine return. Only house-keep forward.

- F-FABLE-2: invalidate a stale round-trip anchor after a trusted
  own-report clear-jump commits. Detection sets a slot marker in the
  clear-jump branch; the marker is popped early (before sanitize/merge,
  no cache leak) and the anchor is popped only AFTER the payload commits
  and passes the significance gate, so a rejected payload never triggers
  it (F-CODEX-7 semantics preserved by placement). The polling test-double
  fallback strips the marker too.

- F-CODEX-1 residual: extend the speed gate with ``is_reliable_fix(existing)``
  so it fires only when BOTH endpoints carry real measured accuracy. Gating
  a fix against an estimated 200 m fallback endpoint would reject it against
  a phantom position and strand the tracker on the estimate.

Tests: 12 new cases across round-trip and speed-gate, each with a mutation
counter-proof (reverting the fix turns the paired test red). Known bounded
trade-offs (F-1/F-2/F-3, all TTL/radius-bounded) are documented in-code and
deferred to a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cache): clear round-trip anchors of propagated shared devices (F-CODEX-9)

A trusted own-report clear jump popped the round-trip anchor only for the
source device_id. update_device_cache then propagates the same fresh trusted
location to sibling device ids that share the identity_key
(_propagate_location_to_shared_devices), leaving their _round_trip_anchors
stale. A later crowd echo near the old anchor for a propagated target could take
the round-trip recovery path and roll that target back to the stale position,
even though the trusted location was already propagated there.

Fix (rides the propagation data-flow, SA-8d):
- _propagate_location_to_shared_devices gains supersede_anchors (default False);
  inside the loop, after a target adopts the fresh location (past the freshness
  gate), its stale anchor is popped too. A fresher target that skips propagation
  keeps its own anchor.
- The supersede marker is now set on any trusted own clear-jump, not only when
  the source holds an anchor: a propagated sibling may hold a stale anchor even
  when the source does not. All anchor pops are idempotent, so marking without a
  source anchor is a harmless no-op on the source side.

Tests: shared-device invalidation, fresher-target preservation (scoping),
source-without-anchor sibling clear, and the rewritten fusion-marker contract;
each with a mutation guard. Behavioral proofs kept within ROUND_TRIP_TTL_S so
they are live, not TTL-vacuous.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci: branch-neutral semantic-release + three-file version sync

Replace the hard-wired branch = "main" with a [tool.semantic_release.branches.release]
regex that matches main and any X.Y maintenance line, add tag_format = "{version}"
to anchor on the recent tag convention, and add version_variables so const.py and
manifest.json are stamped alongside pyproject.toml. Drop the INTEGRATION_VERSION type
annotation (the version_variables regex cannot match NAME: str = "x").

Update the two source-parsing guard tests (test_hacs_validation, test_hypothesis_properties)
to the annotation-free form; they now also guard against re-introducing the annotation.

Make release.yml trigger workflow_dispatch-only (runtime branch choice, no hard-wired name).

* ci: draft-handshake release stamping (protection-aware)

Workflow A (release.yml) now only PROPOSES the next version via
`semantic-release version --print` and opens a DRAFT release pinned to the
dispatched branch (--target); the one-shot version+publish and inline ZIP
steps are removed. Nothing releases automatically.

Workflow B (release-stamp.yml, new) fires on release:published, stamps the
exact published tag into the three version literals via script/stamp_version.py
(new; deterministic, stdlib-only; exact-one-hit assertion per file; tag-format
validation X.Y.Z[bN][.N], no `v` prefix), commits with [skip ci], pushes
protection-aware (direct push; on a 403/protected-branch rejection only, falls
back to a short-lived PR + auto-merge, dormant today), and rebuilds+uploads the
HACS ZIP from the stamped tree so the installed asset carries the right number.

The stamp mirrors the maintainer-chosen tag verbatim (honours hand-picked and
beta `bN` tags), so beta/final isolation across forks needs no PSR prerelease
config. All event-derived values (tag_name, target_commitish) reach shell only
via env indirection, never `${{ }}` in run: blocks.

Also fixes a latent dry-run bug: python-semantic-release v10 requires the
global --noop BEFORE the subcommand (`version --noop` / `changelog --noop` are
not valid options).

Refs #177, #1199

* chore(ci): retrigger checks

* ci: honest version proposal + stamp regression tests (Codex #1199)

Workflow A (release.yml) now detects branch-name-agnostically when
semantic-release cannot propose for a line (PEP 440 maintenance tags it
cannot parse) by comparing its --print-last-released view with the real
topological line tip (git describe), and opens an empty hand-tag draft
instead of a stale number. It also aborts honestly when no release is
due (proposal equals the last released version) instead of drafting an
already-released tag.

Add tests/test_stamp_version.py: VERSION_RE against the real tag matrix
(SemVer + PEP 440 betas/four-segment), the three per-file stampers
(table/key scoping + exact-one-hit), and a byte-parity lock between
VERSION_RE and the mirrored shell guard in release-stamp.yml.

Fix stamp_const: the greedy trailing whitespace match deleted the blank
line after INTEGRATION_VERSION on every stamp; restrict it to horizontal
whitespace so the surrounding layout is preserved.

Addresses the two Codex P2 findings on #1199 (no-release proposal guard;
PEP 440 tags vs. SemVer-only proposal).

* ci: HACS zip_release packaging + PEP 440 tag order (Codex #1199)

Option A (HACS packaging): set zip_release=true + filename in hacs.json and
build the release ZIP from the domain *contents* (cd into the domain dir, zip
`.`) so manifest.json sits at the ZIP root. HACS extracts the asset directly
into custom_components/googlefindmy/; the old parent-dir zip nested it one
level too deep. Without this HACS installed the git source archive and ignored
the stamped ZIP, so the version stamp never reached users.

F-NEW-2 (tag regex): reorder VERSION_RE and the byte-parity shell guard to
(\.N)?([ab]N)? so the numeric release tuple precedes the prerelease per PEP
440 -- now accepts 1.7.14.1b1 and rejects 1.7.14b1.1 (both previously wrong).

Tests: add a ZIP-structure regression guard + zip_release/filename coupling
check, and the two boundary tags to the VERSION_RE matrix.

* fix(release): stamp from the published tag, resolve owning branch robustly

release-stamp.yml checked out github.event.release.target_commitish, which
GitHub documents as unused once the git tag already exists (it then defaults
to the repo default branch). For a release from a pre-existing hand tag on a
maintenance line, that stamped and packaged the wrong tree. The bug was masked
on jleinenbach only because its default branch happens to be the 1.7 line; it
would fire on the planned BSkando transfer (different default branch).

- Check out github.event.release.tag_name (the exact published commit) so the
  stamped tree and the uploaded HACS ZIP always match the release.
- Resolve the stamp-push target branch from the tag commit itself
  (git branch -r --contains), honoring target_commitish only when it is a real
  owning branch; push fast-forward only and skip gracefully (::notice::) when
  the tag is behind the branch tip -- never force-push a maintenance branch.
- Order the push error classification protected-before-non-ff (a protected
  rejection also contains 'rejected').
- Drop the now-obsolete SHA exit-1 guard (it aborted before the ZIP upload and
  the push target is no longer taken raw from target_commitish).
- Add a regression guard test pinning the tag_name checkout and the branch
  resolution (CA-WORKFLOW-EVENT-PAYLOAD-CONTRACT-001).

* fix(release): drop stale target_commitish tiebreaker in branch resolution

The owning-branch resolution honored the stale release.target_commitish
hint before the single-candidate check. When the published tag sits on
several branches (e.g. main + 1.7 on a non-diverged maintenance line),
the stale default-branch hint was among the candidates and got picked,
stamping the wrong branch. GitHub documents target_commitish as unused
once the tag already exists (it defaults to the repo default branch).

Remove the hint tiebreaker entirely and the now-dead TARGET_BRANCH env:
stamp only when exactly one branch owns the tag, otherwise skip safely
(the HACS ZIP asset already carries the version). This heals both the
tie case (ambiguous -> safe skip) and the diverged case (single owner ->
correct), and removes the failure source instead of adding a heuristic.

Tighten the workflow guard test to assert TARGET_BRANCH is gone, closing
the gap the prior 'git branch -r --contains present' check left open.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4128f7a46f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1228 to +1232
anchors[device_id] = {
"lat": existing_lat,
"lon": existing_lon,
"ts": new_ts,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer round-trip anchor mutations until commit

When an incoming crowd fix has an invalid future last_seen, _apply_weighted_location_fusion() runs before _is_significant_update() in update_device_cache(), so this assignment can create a round-trip anchor for a payload that is rejected a few lines later and never committed. If the tracker subsequently moves away, that phantom anchor can still recover a later crowd echo back to the old position even though the A→B jump that created it was discarded; mirror the supersede handling and defer anchor creation/consumption until after significance validation and cache commit.

Useful? React with 👍 / 👎.

Comment on lines +144 to +146
if ! gh release create "$PLACEHOLDER" --draft --target "$REF_NAME" \
--generate-notes \
--title "DRAFT - replace tag with your version (e.g. 1.7.14 or 1.7.14b1) before publishing"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid creating placeholder release tags

In hand-tag mode this still creates a real set-release-tag git tag as soon as the draft is opened: the gh release create docs state that if the tag does not exist, it is automatically created and --target selects its branch. Because release-stamp.yml only validates the tag on publish, a saved/deleted/edited draft can leave this invalid tag on the maintenance branch, where git describe --tags can later see it as the line tip and keep routing future releases into the hand-tag path or collide with the placeholder. Use a real version tag or another draft mechanism that does not create a placeholder tag.

Useful? React with 👍 / 👎.

…Fund A) + release-tip VERSION_RE guard (Fund B) (#1200)

* fix(cache): defer round-trip anchor seed/consume to post-commit (Fund A)

The round-trip anchor store was seeded (accept path) and consumed (recovery path) inside _apply_weighted_location_fusion, before the significance gate and before the payload commits. A payload the significance gate later rejects (early return) then left a phantom anchor (seed) or a prematurely burnt anchor (consume, the symmetric D1 case) behind.

Defer both mutations, mirroring the existing _supersede_round_trip_anchor pattern: the fusion records the intent as a marker on new_data (_round_trip_anchor_seed / _round_trip_anchor_consume); update_device_cache pops the markers early (before sanitize/merge, so they never leak into the cached row) and applies them only after the commit and the significance gate. A rejected payload thus never touches the anchor store; F-CODEX-7 semantics hold automatically via the early return.

The preapply paths (locate.py, polling.py) share the same slot object through to update_device_cache, so the marker survives without extra plumbing; the polling test-double fallback strips the markers for hygiene.

Tests: the existing round-trip suite exercised the fusion unbound, so the fusion-then-significance ordering was never driven and no test could catch this. New integration tests drive update_device_cache for both axes (seed + consume) with the ordering invariant, a positive control, and a re-runnable mutation cross-check; the existing unbound-fusion tests now assert the intent marker on new_data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(release): gate REAL_TIP against VERSION_RE (Fund B hardening)

Codex flagged gh release create --draft as leaving a real git tag that git describe --tags would misread. A live non-destructive test on this fork refuted the premise: a --draft release creates no git tag (git ls-remote --tags empty, the tag ref returns Not Found, GitHub stores an untagged-... draft). No code fix of the alleged path is made.

Independently, gate REAL_TIP (git describe --tags) against the canonical PEP 440 VERSION_RE (SSOT script/stamp_version.py, byte-mirrored in release-stamp.yml), so a foreign non-version line tip (e.g. a stray set-release-tag, nightly, v2) can no longer poison the PSR routing comparison and spuriously open a hand-tag draft. PEP 440 beta/four-segment tags still match VERSION_RE, so the intended MODE=hand protection is preserved.

Extend the byte-parity guard test to cover both workflows, plus a deterministic gate-behaviour test (foreign tip blanked, PEP 440 tip kept).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@jleinenbach

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7756ee7ca3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +635 to +637
self._propagate_location_to_shared_devices(
device_id, slot, supersede_anchors=supersede_anchor
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate round-trip anchors to shared trackers

When two device ids share the same identity_key, an accepted A→B jump on one id is propagated to the sibling by the call below, but the new anchor_seed is only stored for the source device_id just above. If the next B→A return report arrives under the propagated sibling id, the speed gate will find no anchor for that id and reject the legitimate round trip, leaving the shared tracker stranded at B. Pass the seed through the propagation path for targets that actually adopt the fresh B row.

Useful? React with 👍 / 👎.

Comment on lines +136 to +137
if ! gh release create "$PROPOSED" --draft --target "$REF_NAME" \
--generate-notes --title "$PROPOSED"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid creating tags for draft-only releases

This draft step is not tag-neutral: I checked the GitHub CLI manual, and gh release create automatically creates a missing git tag, with --target only choosing where that tag points (https://cli.github.com/manual/gh_release_create). If a maintainer uses this new proposal flow and then abandons/deletes the draft instead of publishing it, the release-stamp workflow never runs but the version tag already exists, so later release calculation can treat the version as released while the three stamped files remain stale.

Useful? React with 👍 / 👎.

… A) (#205) (#1201)

When two device ids share one physical tracker (same identity_key), an
accepted A->B jump seeds a round-trip anchor at the pre-jump position A,
but only for the source device_id. _propagate_location_to_shared_devices
adopts the fresh B row for every sibling and (since #1198/F-CODEX-9)
propagates the supersede pop, but never the seed. The recovery read keys
hard on device_id, so a later B->A return arriving under a sibling id
finds no anchor, the speed gate rejects it as a teleport, and the shared
tracker is stranded at B.

Propagate the seed on the same data-flow, mirroring the F-CODEX-9
supersede block: a new seed_anchors parameter seeds each adopting sibling
(past the freshness gate) with an independent copy of the source-A anchor.
No lazy-init is needed in the propagation block: a non-None seed_anchors
means the source post-commit seed block already initialized the store.

SA-8 breadth sweep: the recovery consume pop is deliberately NOT
propagated (a B->A return on one sibling must not delete another
sibling's still-legitimate A-anchor). Only seed and supersede are
propagation-symmetric; they are mutually exclusive per report (accept-path
seed vs own-report supersede), so no target is double-mutated.

Tests: a mutation-sharp integration test (A->B accept seeds the sibling,
B->A return under the sibling recovers) and a scoping test (a fresher
sibling that skips the freshness gate does not inherit the seed), each
with a re-runnable mutation counter-proof.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@jleinenbach

Copy link
Copy Markdown
Collaborator Author

Codex answers for #205 (reviewed commit 7756ee7)

Finding A (cache.py, propagate round-trip anchors to shared trackers): real, fixed

Confirmed and fixed. The source seeds _anchors[device_id] only, while _propagate_location_to_shared_devices adopts the fresh B row for every sibling and (since #1198) propagates the supersede pop, but never the seed. The recovery read keys hard on device_id, so a B->A return under a sibling id finds no anchor and the speed gate strands the shared tracker at B.

Fix (PR follows): a new seed_anchors parameter propagates the source-A anchor to each adopting sibling as an independent copy (_anchors[target_id] = dict(anchor_seed)), symmetric to the F-CODEX-9 supersede block right above it and riding the same propagation data-flow (only targets past the freshness gate).

SA-8 design note beyond the flagged line: the recovery consume pop is deliberately NOT propagated, because a return on one sibling would delete another sibling's still-legitimate A-anchor. Only seed and supersede are propagation-symmetric; they are mutually exclusive per report, so no target is double-mutated. New mutation-sharp integration test + scoping test cover both the propagation and the fresher-sibling skip.

Finding B (release.yml, avoid creating tags for draft-only releases): false positive

The premise that gh release create --draft immediately creates a real git tag does not hold. A live, non-destructive check on this fork:

  • gh release create <probe-tag> --draft -> GitHub stored an untagged-... draft, no tag
  • git ls-remote --tags after -> empty
  • GET /git/refs/tags/<probe-tag> -> Not Found

So a draft release does not create a git tag; git describe --tags cannot pick it up. The in-code comment "the tag is only created when the draft is published" is correct, and no code change is made.

Independently of the (refuted) draft-tag path, REAL_TIP (git describe --tags) is already gated against the canonical PEP 440 VERSION_RE (SSOT script/stamp_version.py, byte-mirrored in release-stamp.yml), so a foreign non-version tip cannot poison PSR routing. A stale draft whose name is a valid version collides on the next run and is caught by the existing if ! gh release create ... exit 1 fail-safe, never a silent wrong action.

@jleinenbach

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6bd544c39

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1233 to +1234
if delta_anchor >= 0:
anchors.pop(device_id, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve anchors on unrelated rejected fixes

When an active A->B round-trip anchor receives a different rejected crowd fix C within the TTL but outside the return radius, delta_anchor >= 0 is true, so this deletes the anchor even though it has not expired. A later genuine B->A return within the original 15-minute window then has no anchor and is speed-gated, defeating the new recovery feature whenever a noisy report arrives between the trip legs. Please only expire the anchor when the TTL has elapsed (or otherwise leave it in place for non-return rejects).

Useful? React with 👍 / 👎.

@jleinenbach jleinenbach Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding: "Preserve anchors on unrelated rejected fixes" — CONFIRMED, fixed.

You are right. In the speed-gate reject branch, the housekeeping pop guarded the
deletion on delta_anchor >= 0, which conflated two distinct cases:

  • a genuine TTL expiry (delta_anchor > ROUND_TRIP_TTL_S), and
  • an unrelated reject that is still inside the TTL window but landed outside the
    return radius (0 <= delta_anchor <= ROUND_TRIP_TTL_S and
    return_dist > ROUND_TRIP_ANCHOR_RADIUS_M).

The second case is exactly the noisy-report-between-the-trip-legs scenario you
describe: a still-valid A->B anchor was dropped, so a later genuine B->A return
within the original 15-minute window found no anchor and was speed-gated,
defeating the recovery feature.

Fix: the housekeeping pop now fires only on a genuine TTL expiry
(delta_anchor > ROUND_TRIP_TTL_S). A within-TTL reject outside the radius, and
the previously handled out-of-order case (delta_anchor < 0), both leave the
anchor in place; it expires naturally once the TTL elapses. Recovery stays
one-shot and radius-gated, so keeping the anchor longer cannot open a ping-pong
(DF-1 bulwark unchanged).

Covered by a new behavioral test: an active anchor survives a within-TTL
out-of-radius reject, and a subsequent genuine near-A return then recovers
(mutation check: reverting the guard turns the test red). Genuine-expiry pop and
the out-of-order preservation keep their existing tests green.

Fixed on jleinenbach:1.7 in PR #1202.

…r rejects (#1202)

The speed-gate reject branch house-kept the round-trip anchor whenever
delta_anchor >= 0, conflating a genuine TTL expiry with an unrelated crowd
fix that landed within the TTL window but outside the return radius. A noisy
report between the trip legs therefore dropped a still-valid A->B anchor, so a
later genuine B->A return within the original 15-minute window found no anchor,
was speed-gated, and the recovery feature was defeated.

Gate the housekeeping pop on a genuine TTL expiry (delta_anchor >
ROUND_TRIP_TTL_S). A within-TTL far reject and an out-of-order report
(delta_anchor < 0) both leave the anchor in place; it expires naturally by TTL.
The single "> TTL" test subsumes the old F-FABLE-1 ">= 0" guard. Recovery stays
double-gated (return_dist <= radius AND delta_anchor <= TTL) and one-shot, so
keeping the anchor longer cannot open a ping-pong.

Add two behavioral tests (within-TTL far reject preserves the anchor;
subsequent genuine return recovers), each with a re-runnable mutation check.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@jleinenbach

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9880e5ddaa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pyproject.toml
# anchor:
# custom_components/googlefindmy/AGENTS.md ("Version bump touches three files").
version = "1.7.13"
version = "1.7.14.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Poetry version compatible with semantic-release

The new release workflow still invokes poetry run semantic-release version --print[-last-released], but this stamps [tool.poetry] with a four-segment PEP 440 version. I checked the Python Semantic Release 10 docs, which state that the configured project version must be SemVer MAJOR.MINOR.PATCH and that PEP 440 values are not supported; with 1.7.14.0, the PSR commands in .github/workflows/release.yml fail or get suppressed into the hand-tag path, so branch-neutral automatic proposals/dry-runs are broken after this release. Keep the Poetry value SemVer-compatible or avoid running PSR against PEP-440-stamped trees.

Useful? React with 👍 / 👎.

… lines (#1203)

The coupled-triple comment claimed semantic-release keeps all three version
literals in sync automatically on any release line (main and every X.Y
maintenance branch) and that a manual bump is only a fallback. That
contradicts .github/workflows/release.yml (semantic-release only understands
SemVer; PEP 440 maintenance lines, tagged four-segment .N or beta bN, are
silently skipped by PSR, so the maintainer hand-tags there). Rewrite the
comment to match the workflow: PSR auto-sync only on SemVer lines (main); on
PEP 440 maintenance lines the hand-tag + release-stamp.yml stamp
(script/stamp_version.py) is the primary path, not a fallback.

Comment-only change; the version literal and all [tool.*] settings are
unchanged. Addresses the Codex review finding on #205.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@jleinenbach

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22748127c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1157 to +1162
if (
self._speed_gate_enabled()
and not incoming_is_own
and new_acc_measured
and is_reliable_fix(existing)
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require measured accuracy on the cached endpoint

When a user restores a legacy/corrupt cached row with Android's no-accuracy sentinel (for example accuracy: 0.0, or another sub-physical/non-finite value) and no accuracy_estimated flag, this guard still treats the cached endpoint as reliable because is_reliable_fix() only requires accuracy is not None and not estimated. A later valid measured crowd fix after a far move can then be rejected by the new speed gate against that unreliable baseline, preventing the existing self-healing path from accepting the new position; please apply the same finite/>= MIN_PHYSICAL_ACCURACY_M measured-accuracy check to the cached endpoint before gating or seeding anchors.

Useful? React with 👍 / 👎.

…_fix (Codex #205) (#1204)

* fix(coordinator): require finite sub-physical accuracy in is_reliable_fix

is_reliable_fix() only checked accuracy presence + not-estimated, so a
cached/reference endpoint carrying Android's 0.0 no-accuracy sentinel (or
a negative, sub-MIN_PHYSICAL_ACCURACY_M, or non-finite value) WITHOUT the
accuracy_estimated flag was treated as a real, reliable fix. The incoming
side already filters this exact error-code set (math.isfinite + >=
MIN_PHYSICAL_ACCURACY_M), so a genuine measured crowd fix could be
speed-gated against a 0 m phantom reference and dropped (Codex #205,
#1179 poisoning class). The predicate's own docstring ("only a real
measurement") and the gate comment ("BOTH endpoints carry real measured
accuracy") were not backed by the code.

Fix at the predicate level (DRY): is_reliable_fix now also requires a
finite, >= MIN_PHYSICAL_ACCURACY_M, non-bool numeric accuracy, mirroring
the incoming-side new_acc_measured check. This closes the gap at every
call site in one place (speed gate, round-trip anchor seed source A,
last-good retention, both device_tracker last-good sites). has_usable_accuracy
(the display gate) stays lenient so a sanitized estimated/0.0 fix is still
shown. Consistent with the F-2 trade-off: an unreliable reference makes the
gate block fall through rather than gating against a phantom reference.

Four mutation-sharp tests: the predicate rejects 0.0/-1.0/inf/nan/bool;
the speed gate does not fire against a 0.0 cached endpoint; the retention
does not advance a 0.0 last-good over a reliable one; a 0.0 anchor source
A seeds no anchor.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* test(coordinator): pin >= floor boundary in is_reliable_fix (Codex #205 review followup)

Add a boundary assertion for the MIN_PHYSICAL_ACCURACY_M floor: exactly
MIN_PHYSICAL_ACCURACY_M is reliable, a hair below is not. Closes a mutation
hole flagged by the terminal diff review (a >=/> mutation on the floor
check would otherwise go undetected). Verified mutation-sharp locally
(>= -> > turns the boundary test red).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@jleinenbach

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 6844c1d742

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jleinenbach
jleinenbach merged commit a0ecb69 into BSkando:1.7 Jul 18, 2026
18 checks passed
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.

1 participant