fix(occupancy): trust/KPI dataset parity and 1:1 camera-group invariant — phase 2 (#28, #35) #54

Merged
gabogg merged 6 commits from fix/counting-kpi-group-join-cutover into master 2026-09-23 22:16:31 +00:00
Owner

Closes #28
Closes #35

Phase 2 of 2. 🔒 Blocked: waits for #53 to merge. Can proceed in parallel with #52 once #53 lands.

This description replaces the 2026-09-22 draft. The second grilling session (2026-09-22/23) found that every camera group in this deployment has exactly one camera, so the drafted full group-join cutover would change nothing observable while costing a table rebuild, seven query rewrites and a user-facing relabel. That work moved to #62, triggered by the first multi-camera group. This PR keeps only what is wrong today.


1. Trust rules and KPIs read the same dataset (#35)

Five aggregates inner-join counting_cameras with c.is_excluded = 0 AND c.is_active = 1 (occupancy_repository.py get_counts_in_range_async, get_timespan_aggregates_async, get_cycle_peak_occupancy_async, get_cycle_average_occupancy_async, get_bucketed_cycle_flow_async). get_hourly_flow_distribution_async has no join and no filter, so it sums excluded cameras, inactive cameras and orphan rows. It is the sole input to trust rules R1/R2 in evaluate_cycle_integrity_async: the engine deciding whether a cycle is trustworthy reads a different dataset from the one published.

  • Give get_hourly_flow_distribution_async the same join and filter as the KPI aggregates.
  • Delete get_timespan_aggregates_sync (:741) — dead code, no callers.
  • #35's fix (3), collapsing "counts over time" onto one query shape, stays filed separately.

2. Documentation of the 1:1 invariant (#28)

Already pushed on this branch (a663cb2):

  • CONTEXT.md §3 — Camera Group (the HikCentral resource group; _Avoid_: Zone), the one-camera-per-group invariant, Direction (IN/OUT, per event) vs DirectionType (ENTRANCE/EXIT/BIDIRECTIONAL, per camera).
  • ADR 0005 — Passenger flow is counted per camera group, with one camera per group.

The runtime guard that flags multi-camera groups is in #53. With the invariant documented and violations visible, #28's fabricated split can no longer happen silently, so #28 closes here; the multi-camera case is #62.


Acceptance criteria

  • get_hourly_flow_distribution_async applies the same join and exclusion filter as the KPI aggregates.
  • Test: excluding a camera changes the R1/R2 input and the published KPIs identically.
  • Test: an orphan row changes neither.
  • get_timespan_aggregates_sync removed.
  • CONTEXT.md §3 and ADR 0005 committed.
  • Rebased/merged onto master after #53.
  • Full suite green (pytest + node --test).

🤖 Generated with Claude Code

Closes #28 Closes #35 Phase 2 of 2. **🔒 Blocked: waits for #53 to merge.** Can proceed in parallel with #52 once #53 lands. This description replaces the 2026-09-22 draft. The second grilling session (2026-09-22/23) found that **every camera group in this deployment has exactly one camera**, so the drafted full group-join cutover would change nothing observable while costing a table rebuild, seven query rewrites and a user-facing relabel. That work moved to **#62**, triggered by the first multi-camera group. This PR keeps only what is wrong today. --- ## 1. Trust rules and KPIs read the same dataset (#35) Five aggregates inner-join `counting_cameras` with `c.is_excluded = 0 AND c.is_active = 1` (`occupancy_repository.py` `get_counts_in_range_async`, `get_timespan_aggregates_async`, `get_cycle_peak_occupancy_async`, `get_cycle_average_occupancy_async`, `get_bucketed_cycle_flow_async`). `get_hourly_flow_distribution_async` has **no join and no filter**, so it sums excluded cameras, inactive cameras and orphan rows. It is the sole input to trust rules R1/R2 in `evaluate_cycle_integrity_async`: the engine deciding whether a cycle is trustworthy reads a different dataset from the one published. - Give `get_hourly_flow_distribution_async` the same join and filter as the KPI aggregates. - Delete `get_timespan_aggregates_sync` (`:741`) — dead code, no callers. - #35's fix (3), collapsing "counts over time" onto one query shape, stays filed separately. ## 2. Documentation of the 1:1 invariant (#28) Already pushed on this branch (`a663cb2`): - **`CONTEXT.md` §3** — *Camera Group* (the HikCentral resource group; `_Avoid_: Zone`), the **one-camera-per-group invariant**, *Direction* (`IN`/`OUT`, per event) vs *DirectionType* (`ENTRANCE`/`EXIT`/`BIDIRECTIONAL`, per camera). - **ADR 0005** — *Passenger flow is counted per camera group, with one camera per group*. The runtime guard that flags multi-camera groups is in #53. With the invariant documented and violations visible, #28's fabricated split can no longer happen silently, so #28 closes here; the multi-camera case is #62. --- ## Acceptance criteria - [x] `get_hourly_flow_distribution_async` applies the same join and exclusion filter as the KPI aggregates. - [x] Test: excluding a camera changes the R1/R2 input and the published KPIs identically. - [x] Test: an orphan row changes neither. - [x] `get_timespan_aggregates_sync` removed. - [x] `CONTEXT.md` §3 and ADR 0005 committed. - [x] Rebased/merged onto `master` after #53. - [x] Full suite green (pytest + `node --test`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chore(wip): open draft for counting KPI group-join cutover (phase 2 of #28/#29/#35)
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
6d506b1112
WIP scaffold. Phase 2 of the counting-events/camera integrity split: makes the
camera group the join key across the KPI aggregates. Depends on the phase-1
schema landing first. See PR description for scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(occupancy): record per-camera-group counting and the 1:1 invariant
All checks were successful
CI / lint-and-test (pull_request) Successful in 1m11s
a663cb28e3
Adds ADR 0005 and sharpens CONTEXT.md section 3: Camera Group (with the
one-camera-per-group invariant), Direction (per event) vs DirectionType
(per camera, including BIDIRECTIONAL). Zone is retired as a synonym.

Refs #28, #62

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gabogg changed title from WIP: fix(occupancy): camera-group join cutover — phase 2 (#28, #35) to WIP: fix(occupancy): trust/KPI dataset parity and 1:1 camera-group invariant — phase 2 (#28, #35) 2026-09-23 13:57:03 +00:00
Author
Owner

🔒 Blocked — waiting for #53 to merge.

Settled in the grilling session of 2026-09-22/23: merge order is #53 → then #52 and #54 in parallel. #53 rewrites the sync loop (occupancy_service.py:1814-1888) and deletes the webhook counting block, both of which this PR depends on. Do not start implementation until #53 is merged; then merge master into this branch first.

Description updated to the settled scope. The deferred full group-join cutover is #62.

🔒 **Blocked — waiting for #53 to merge.** Settled in the grilling session of 2026-09-22/23: merge order is **#53 → then #52 and #54 in parallel**. #53 rewrites the sync loop (`occupancy_service.py:1814-1888`) and deletes the webhook counting block, both of which this PR depends on. Do not start implementation until #53 is merged; then merge `master` into this branch first. Description updated to the settled scope. The deferred full group-join cutover is #62.
Author
Owner

🔓 Unblocked — #53 merged (1ed0f45, 2026-09-23).

Merging master into this branch (it also brings #63's glossary changes) and starting implementation. Draft status removed at the maintainer's request.

Scope reminder: #58 waits on this PR, and the full group-join cutover is #62.

🔓 **Unblocked — #53 merged** (`1ed0f45`, 2026-09-23). Merging `master` into this branch (it also brings #63's glossary changes) and starting implementation. Draft status removed at the maintainer's request. Scope reminder: #58 waits on this PR, and the full group-join cutover is #62.
gabogg changed title from WIP: fix(occupancy): trust/KPI dataset parity and 1:1 camera-group invariant — phase 2 (#28, #35) to fix(occupancy): trust/KPI dataset parity and 1:1 camera-group invariant — phase 2 (#28, #35) 2026-09-23 18:05:23 +00:00
fix(occupancy): trust rules read the same dataset as the published KPIs (#35)
All checks were successful
CI / lint-and-test (pull_request) Successful in 1m10s
dfb0644f75
get_hourly_flow_distribution_async, the sole input to trust rules R1/R2,
had no join and no filter: it summed events from excluded and inactive
cameras and orphan codes that no KPI counts. A burst on an excluded
camera could flag a cycle as FLAG_BURST_COUNTER_FLUSH while the published
totals ignored it.

It now joins counting_cameras with the same is_excluded = 0 AND
is_active = 1 filter as the KPI aggregates. Tests prove excluding or
deactivating a camera changes the trust input and the KPI totals
identically, an orphan row changes neither, and an excluded camera's
burst no longer flags a trusted cycle.

Also deletes get_timespan_aggregates_sync, which had no callers.

Closes #35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Implemented — dfb0644

  • get_hourly_flow_distribution_async (sole input to trust rules R1/R2) now joins counting_cameras with the same is_excluded = 0 AND is_active = 1 filter as the KPI aggregates.
  • get_timespan_aggregates_sync deleted (no callers).
  • master merged in (brings #53 and #63; CONTEXT.md merged cleanly: Camera Group from this branch sits alongside Camera Group States / Quarantined Counting Event from #53).

New tests/test_trust_dataset_parity.py:

  • excluding a camera changes the trust input and KPI totals identically; so does deactivating one;
  • an orphan row changes neither;
  • an excluded camera's single-hour burst no longer flags a cycle FLAG_BURST_COUNTER_FLUSH (confirmed failing against the old query, passing with the fix).

The half-open cycle window (< cycle_end) of the hourly query is unchanged; the KPI query uses <= end_epoch. They differ only for an event stamped exactly at the cycle boundary. That is left for #58, which removes this query entirely.

Verification: pytest 233 passed / 1 skipped, node --test 59/59, ruff clean, pre-commit hooks passed. #58 can start once this merges.

## Implemented — `dfb0644` - `get_hourly_flow_distribution_async` (sole input to trust rules R1/R2) now joins `counting_cameras` with the same `is_excluded = 0 AND is_active = 1` filter as the KPI aggregates. - `get_timespan_aggregates_sync` deleted (no callers). - `master` merged in (brings #53 and #63; `CONTEXT.md` merged cleanly: *Camera Group* from this branch sits alongside *Camera Group States* / *Quarantined Counting Event* from #53). New `tests/test_trust_dataset_parity.py`: - excluding a camera changes the trust input and KPI totals **identically**; so does deactivating one; - an orphan row changes **neither**; - an excluded camera's single-hour burst no longer flags a cycle `FLAG_BURST_COUNTER_FLUSH` (confirmed failing against the old query, passing with the fix). The half-open cycle window (`< cycle_end`) of the hourly query is unchanged; the KPI query uses `<= end_epoch`. They differ only for an event stamped exactly at the cycle boundary. That is left for #58, which removes this query entirely. **Verification:** pytest **233 passed / 1 skipped**, `node --test` **59/59**, ruff clean, pre-commit hooks passed. #58 can start once this merges.
Author
Owner

Code review — round 1

Reviewed dfb0644 against master (4115881) with two independent passes: Standards (docs/standards/code-standards.md, AGENTS.md, domain-modeling CONTEXT-FORMAT.md / ADR-FORMAT.md, plus a code-smell baseline) and Spec (this PR's description, #35, #28 and its amendment). The Spec pass re-ran the suites in a throwaway worktree: pytest 233 passed / 1 skipped, node --test 59/59. Mutation check: with the old unfiltered query restored, all 4 new tests fail.

Verdict: ready to merge. No P1 on either axis, no P2 on Spec.

Standards

No documented-standard breaches: SQL stays in the repository, annotations intact, tests use real SQLite with nothing mocked.

  • P2, glossary overlap, CONTEXT.md:88 vs :159: the invariant paragraph under Camera Group repeats #53's Multi-camera group bullet almost verbatim ("per-camera figures… are estimates, not measurements"). Keep it in one place.
  • P3, possible Duplicated Code, occupancy_repository.py:1502-1506: JOIN counting_cameras … AND c.is_excluded = 0 AND c.is_active = 1 now appears six times (:973, :994, :1136, :1200, :1506, :1598). #35 exists because copies of this rule drifted; the fix adds another hand copy. Name the concept once.
  • P3, CONTEXT-FORMAT ("define what it IS"), :88: the invariant is a behavioural rule ("The system expects…") inside a definition. :101: the second _Avoid_ sentence is a scope note, not an alias.
  • P3, ADR-FORMAT, ADR 0005 :3: Status: is a body line rather than frontmatter, and the opening context runs past the template's 1–3 sentences. The ADR otherwise meets the criteria, and its multi-camera-flag claim checks out in the code.
  • P3: deleting get_timespan_aggregates_sync is safe (no callers) but outside the #35 commit's stated scope. @pytest.mark.asyncio is redundant under asyncio_mode = "auto" (§4.2), though consistent with the suite.

Spec

All KPI aggregates and the trust input now share the same join and filter; R3/R4 are fed filtered totals at both call sites of evaluate_cycle_integrity_async. #35's fixes (1) and (2) are done, (3) is #58. #28 closing is consistent with the rescope (1:1 invariant, #53's guard, #62). No scope creep.

  • P3, not reproduced: #28's amendment folded the second direction derivation in /simulate (occupancy_controller.py:385-386, direction_type … or cams) into phase 2; the rescoped PR neither fixes nor explicitly defers it. It is already item 7 of #62's scope.
  • P3, reasoned: the hourly query ends at < cycle_end, the KPI queries at <= end. Only events in a cycle's final millisecond differ, and that can only flip R2 on a cycle sitting at the 35% line. Declared as left to #58.
  • P3: get_earliest_event_epoch_async and get_passenger_flow_telemetry_async are still unfiltered. Neither feeds a KPI or a trust rule.

Standards: 6 findings, worst P2 (glossary overlap). Spec: 3 findings, worst P3. All being addressed in a follow-up commit on this branch.

## Code review — round 1 Reviewed `dfb0644` against `master` (`4115881`) with two independent passes: **Standards** (`docs/standards/code-standards.md`, `AGENTS.md`, domain-modeling `CONTEXT-FORMAT.md` / `ADR-FORMAT.md`, plus a code-smell baseline) and **Spec** (this PR's description, #35, #28 and its amendment). The Spec pass re-ran the suites in a throwaway worktree: pytest **233 passed / 1 skipped**, `node --test` **59/59**. **Mutation check:** with the old unfiltered query restored, all 4 new tests fail. **Verdict: ready to merge.** No P1 on either axis, no P2 on Spec. ## Standards No documented-standard breaches: SQL stays in the repository, annotations intact, tests use real SQLite with nothing mocked. - **P2, glossary overlap,** `CONTEXT.md:88` vs `:159`: the invariant paragraph under *Camera Group* repeats #53's *Multi-camera group* bullet almost verbatim ("per-camera figures… are estimates, not measurements"). Keep it in one place. - **P3, possible Duplicated Code,** `occupancy_repository.py:1502-1506`: `JOIN counting_cameras … AND c.is_excluded = 0 AND c.is_active = 1` now appears six times (`:973, :994, :1136, :1200, :1506, :1598`). #35 exists because copies of this rule drifted; the fix adds another hand copy. Name the concept once. - **P3, CONTEXT-FORMAT** ("define what it IS"), `:88`: the invariant is a behavioural rule ("The system expects…") inside a definition. `:101`: the second `_Avoid_` sentence is a scope note, not an alias. - **P3, ADR-FORMAT,** ADR 0005 `:3`: `Status:` is a body line rather than frontmatter, and the opening context runs past the template's 1–3 sentences. The ADR otherwise meets the criteria, and its multi-camera-flag claim checks out in the code. - **P3:** deleting `get_timespan_aggregates_sync` is safe (no callers) but outside the #35 commit's stated scope. `@pytest.mark.asyncio` is redundant under `asyncio_mode = "auto"` (§4.2), though consistent with the suite. ## Spec All KPI aggregates and the trust input now share the same join and filter; R3/R4 are fed filtered totals at both call sites of `evaluate_cycle_integrity_async`. #35's fixes (1) and (2) are done, (3) is #58. #28 closing is consistent with the rescope (1:1 invariant, #53's guard, #62). No scope creep. - **P3, not reproduced:** #28's amendment folded the second direction derivation in `/simulate` (`occupancy_controller.py:385-386`, `direction_type … or cams`) into phase 2; the rescoped PR neither fixes nor explicitly defers it. It is already item 7 of #62's scope. - **P3, reasoned:** the hourly query ends at `< cycle_end`, the KPI queries at `<= end`. Only events in a cycle's final millisecond differ, and that can only flip R2 on a cycle sitting at the 35% line. Declared as left to #58. - **P3:** `get_earliest_event_epoch_async` and `get_passenger_flow_telemetry_async` are still unfiltered. Neither feeds a KPI or a trust rule. --- **Standards: 6 findings, worst P2** (glossary overlap). **Spec: 3 findings, worst P3.** All being addressed in a follow-up commit on this branch.
fix(occupancy): address PR #54 review round 1
All checks were successful
CI / lint-and-test (pull_request) Successful in 1m15s
990d495c25
- Name the counted-camera rule once. The join and filter shared by every
  published figure and trust-rule input (`is_excluded = 0 AND
  is_active = 1`) were six hand copies; #35 happened because copies of
  this rule drifted. They are now `_COUNTED_CAMERA_JOIN` and
  `_COUNTED_CAMERA_FILTER` in the repository.
- The hourly trust input now ends its window at `<= cycle_end`, like the
  KPI queries, so an event stamped at the cycle boundary is counted by
  both. New test pins both edges.
- Document why `get_earliest_event_epoch_async` and
  `get_passenger_flow_telemetry_async` are intentionally unfiltered
  (reconciliation bound; raw audit/export log).
- CONTEXT.md: Camera Group no longer restates the multi-camera consequence
  that Camera Group States already defines; the invariant is phrased as a
  definition and points to ADR 0005; the DirectionType _Avoid_ line no
  longer carries a scope note.
- ADR 0005: status as frontmatter, a two-sentence summary, detail moved
  into a Context section.
- Drop redundant @pytest.mark.asyncio markers (asyncio_mode = "auto").

Scope note: this PR also deleted get_timespan_aggregates_sync in the
previous commit (no callers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Review round 1 addressed — 990d495

Standards

  • P2, glossary overlap: fixed. Camera Group no longer restates the multi-camera consequence; it defines the term, states that each group holds one camera in this deployment (ADR 0005), and points to Camera Group States, which keeps the single statement of what happens otherwise.
  • P3, six hand copies of the counted-camera rule: fixed. The join and the filter are now _COUNTED_CAMERA_JOIN / _COUNTED_CAMERA_FILTER in occupancy_repository.py, with a comment naming the concept. All six queries use them.
  • P3, CONTEXT-FORMAT: fixed. The invariant is phrased as part of the definition, and the DirectionType _Avoid_ line no longer carries a scope note.
  • P3, ADR-FORMAT: fixed. ADR 0005 has status/date frontmatter, a two-sentence summary, and the detail moved into a Context section.
  • P3, commit scope: noted. The deletion of get_timespan_aggregates_sync is now called out in the follow-up commit message and was already in this PR's description.
  • P3, redundant @pytest.mark.asyncio: fixed in this PR's test file (asyncio_mode = "auto").

Spec

  • P3, < vs <= window: fixed. The hourly trust input now ends at <= cycle_end, like the KPI queries. New test test_window_bounds_match_at_the_cycle_edges pins both edges. (#58 will move trust to get_bucketed_cycle_flow_async, which still uses <; #58 must keep this parity.)
  • P3, unfiltered queries: documented as intentional. get_earliest_event_epoch_async only bounds where reconciliation starts; get_passenger_flow_telemetry_async is the raw audit, export and recent-passages log. Both docstrings now say why the counted-camera rule doesn't apply.
  • P3, /simulate direction derivation: stays deferred; it is item 7 of #62's scope ("Simulator… drop its or cams fallback").

Verification: pytest 234 passed / 1 skipped, tests/test_docs.py 7/7, ruff clean, pre-commit hooks passed.

## Review round 1 addressed — `990d495` **Standards** - **P2, glossary overlap: fixed.** *Camera Group* no longer restates the multi-camera consequence; it defines the term, states that each group holds one camera in this deployment (ADR 0005), and points to *Camera Group States*, which keeps the single statement of what happens otherwise. - **P3, six hand copies of the counted-camera rule: fixed.** The join and the filter are now `_COUNTED_CAMERA_JOIN` / `_COUNTED_CAMERA_FILTER` in `occupancy_repository.py`, with a comment naming the concept. All six queries use them. - **P3, CONTEXT-FORMAT: fixed.** The invariant is phrased as part of the definition, and the DirectionType `_Avoid_` line no longer carries a scope note. - **P3, ADR-FORMAT: fixed.** ADR 0005 has `status`/`date` frontmatter, a two-sentence summary, and the detail moved into a *Context* section. - **P3, commit scope: noted.** The deletion of `get_timespan_aggregates_sync` is now called out in the follow-up commit message and was already in this PR's description. - **P3, redundant `@pytest.mark.asyncio`: fixed** in this PR's test file (`asyncio_mode = "auto"`). **Spec** - **P3, `<` vs `<=` window: fixed.** The hourly trust input now ends at `<= cycle_end`, like the KPI queries. New test `test_window_bounds_match_at_the_cycle_edges` pins both edges. (#58 will move trust to `get_bucketed_cycle_flow_async`, which still uses `<`; #58 must keep this parity.) - **P3, unfiltered queries: documented as intentional.** `get_earliest_event_epoch_async` only bounds where reconciliation starts; `get_passenger_flow_telemetry_async` is the raw audit, export and recent-passages log. Both docstrings now say why the counted-camera rule doesn't apply. - **P3, `/simulate` direction derivation:** stays deferred; it is item 7 of #62's scope ("Simulator… drop its `or cams` fallback"). **Verification:** pytest **234 passed / 1 skipped**, `tests/test_docs.py` 7/7, ruff clean, pre-commit hooks passed.
Author
Owner

Code review — round 2 (follow-up)

Reviewed 990d495 against master with the same two independent passes. CI green; merges cleanly.

Verdict: ready to merge. Every round-1 finding holds; no P1 or P2 on either axis.

Standards

Round 1: all 6 hold (glossary overlap, one counted-camera rule, CONTEXT-FORMAT, ADR-FORMAT, commit scope, markers). Interpolating the _COUNTED_CAMERA_* constants into f-string SQL is compliant: code-standards only requires SQL in repositories with values bound via ?, which they are. No query had literal braces, and nothing parses docs/adr.

  • P3: the new comment claims every published figure goes through the one rule, but top entrances/exits re-apply it in Python (:1061, :1084). Pre-existing third copy.
  • P3: get_bucketed_cycle_flow_async still ends at < (:1608) while the hourly query now ends at <=.
  • P3: the test helper takes both hour and at for the same timestamp.
  • P3: "does not allow for… It is still counted" in Camera Group States reads as a contradiction.

Spec

Round 1: all 3 hold. Real callers pass cycle_end as reset - 0.001, start + 86400 - 0.001 or t; a probe gave buckets 0–23 only, so R1/R2 are unaffected by <=. Five of the refactored queries produce byte-identical SQL, and the sixth differs only by the intended <=. pytest 234 passed; a mutation back to < fails the boundary test.

  • P3: the "#58 must keep <= parity" caveat lived only in a PR comment, not in #58 or the triage record.
  • P3, reproduced: the boundary test uses start + 24 h exactly, which no caller passes; the end event lands in a 25th bucket the volume-sum assertion can't see.

Standards: 4 findings, worst P3. Spec: 2 findings, worst P3. All filed in #66; the parity caveat is also posted on #58. Merging.

## Code review — round 2 (follow-up) Reviewed `990d495` against `master` with the same two independent passes. CI green; merges cleanly. **Verdict: ready to merge.** Every round-1 finding holds; no P1 or P2 on either axis. ## Standards Round 1: all 6 hold (glossary overlap, one counted-camera rule, CONTEXT-FORMAT, ADR-FORMAT, commit scope, markers). Interpolating the `_COUNTED_CAMERA_*` constants into f-string SQL is compliant: code-standards only requires SQL in repositories with values bound via `?`, which they are. No query had literal braces, and nothing parses `docs/adr`. - **P3:** the new comment claims every published figure goes through the one rule, but top entrances/exits re-apply it in Python (`:1061`, `:1084`). Pre-existing third copy. - **P3:** `get_bucketed_cycle_flow_async` still ends at `<` (`:1608`) while the hourly query now ends at `<=`. - **P3:** the test helper takes both `hour` and `at` for the same timestamp. - **P3:** "does not allow for… It is still counted" in *Camera Group States* reads as a contradiction. ## Spec Round 1: all 3 hold. Real callers pass `cycle_end` as `reset - 0.001`, `start + 86400 - 0.001` or `t`; a probe gave buckets 0–23 only, so R1/R2 are unaffected by `<=`. Five of the refactored queries produce byte-identical SQL, and the sixth differs only by the intended `<=`. pytest 234 passed; a mutation back to `<` fails the boundary test. - **P3:** the "#58 must keep `<=` parity" caveat lived only in a PR comment, not in #58 or the triage record. - **P3, reproduced:** the boundary test uses `start + 24 h` exactly, which no caller passes; the end event lands in a 25th bucket the volume-sum assertion can't see. --- **Standards: 4 findings, worst P3. Spec: 2 findings, worst P3.** All filed in **#66**; the parity caveat is also posted on **#58**. Merging.
gabogg merged commit c2cb2e6714 into master 2026-09-23 22:16:31 +00:00
gabogg deleted branch fix/counting-kpi-group-join-cutover 2026-09-23 22:16:32 +00:00
Sign in to join this conversation.
No description provided.