GET /api/meta/capabilities dropped group membership at the wire: it
shipped a flat group-id list and a flat leaf list with no boundary
between them, so a native client could not tell which leaves live
under which group, or which group is the fenced sensitive tier. That
made it unsafe for the client to offer a "select all" affordance for
any group, since it could not mirror the server's fence against
bundling more than one sensitive leaf behind a single control.
Add SHARE_GROUPS to the report-selection catalogue, derived from the
existing REPORT_GROUPS/MEASUREMENT_LEAF_GROUP/STRUCTURED_LEAF_GROUP
maps: each group now carries its member leaf ids, and the group whose
id equals SENSITIVE_GROUP_ID carries sensitive: true. share.groups in
the capabilities response now ships this shape instead of bare ids;
share.leaves is unchanged. Update the OpenAPI schema and regenerate
docs/api/openapi.yaml.
Add an invariant test asserting every leaf in the flat leaves set
appears in exactly one group and no group carries a leaf outside that
set, plus a check that exactly one group is flagged sensitive.
GET /api/insights/rhythm-events was never in the spec, so a client had
nothing to generate a type from but the three-value ECG classification
next door — reusing it would drop VERY_LOW / FIRED. Register the route
with its own RhythmEventClassification carrying all six
RhythmClassification values; the ECG enum stays untouched at three.
The canonical picker keyed a session on sport, start, end and duration. Only the
workout list selected the end timestamp; the Coach narration, the per-sport
baseline and the deep-link cluster passed rows without it, so those rows carried
no session identity at all and the duplicate survived. One record counted once in
the list and two or three times everywhere else, including in the averages a
person is compared against.
Drop the end timestamp from the identity. On a workout with pauses it measures
wall-clock time while the duration counts only moving time, so two re-sends of
one session can differ in that field alone and the stricter key rejected a
genuine match. It also bought nothing in exchange: nobody starts a second
workout of the same sport in the same instant with the same duration.
The deep-link cluster now selects the duration it needs. A structural guard
freezes the call-site set and records which file owns each caller's select, so a
new surface cannot join the set without fetching the identity, and the identity
cannot grow a field only some callers fetch.
- preserve prior rolling identity independently of local-day flooring
- scope exact resolution by schedule and era while advancing resolved grids
- include open rolling slots for display without widening compliance bands
- select active canonical recurrence slots instead of rebuilding from tick day
- scope intake suppression and dedup metadata to the occurrence local date
- lock and rotate refresh families inside one database transaction
- revoke CAS losers and insert successors before commit
- sign refresh artifacts only after committed state
- Normalize IPv6 literals before transition-prefix inspection
- Reject mapped, compatible, 6to4, and NAT64 private embeddings
- Share the verdict with pinned public and operator dispatchers
- delete current and legacy HealthLog static page and data caches
- preserve unrelated CacheStorage namespaces during activation and logout
- keep IndexedDB query-cache removal in the session-end boundary
- document expected cache-isolation and transition-form failures
- record preserved public and redirect behavior
- capture atomic task commits and verification evidence
- refuse standard embedded-private IPv6 resolver answers pre-connect
- preserve public IPv6 to IPv4 fallback connectivity
- prove followed redirects repeat the pinned DNS verdict