Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**Author**: Paul Calnon
**License**: MIT License
**Version**: 0.5.0
**Last Updated**: 2026-06-30
**Last Updated**: 2026-07-04

---

Expand Down Expand Up @@ -260,7 +260,7 @@ For any new `prometheus_client` `Counter` / `Gauge` / `Histogram` / `Summary` /
- `register_info_or_update(name, description, **labels)` — sugar for the `Info` two-step register-then-`.info({...})` pattern.
- `lazy_register_or_reuse(...)` — for the lazy-init-with-`None`-sentinel pattern.

Tests touching these collectors should use `juniper_observability.testing.reset_prometheus_registry`. Existing examples in this repo: `src/observability.py:_ensure_canopy_metrics`, `src/main.py` (browser WS metrics), `src/adapter_validation.py`, `src/frontend/dashboard_manager.py`. See [the design doc in juniper-ml](https://github.com/pcalnon/juniper-ml/blob/main/notes/observability/REGISTER_OR_REUSE_HELPER_DESIGN_2026-05-05.md) for the rationale.
Tests touching these collectors should use `juniper_observability.testing.reset_prometheus_registry`. Existing examples in this repo: `src/observability.py:_ensure_canopy_metrics`, `src/main.py` (browser WS metrics), `src/adapter_validation.py`, `src/frontend/dashboard_manager.py`. See [the design doc in juniper-ml](https://github.com/pcalnon/juniper-ml/blob/main/notes/observability/JUNIPER_2026-05-05_JUNIPER-ML_REGISTER-OR-REUSE-HELPER-DESIGN.md) for the rationale.

### CI/CD

Expand Down
10 changes: 5 additions & 5 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/ENVIRONMENT_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ This guide covers complete environment setup for Juniper Canopy development and
> Juniper conda environments are **versioned across rebuilds**. This project's
> live environment is currently **`JuniperCanopy1`**; each full rebuild (per the
> Juniper ecosystem conda-env rebuild procedure,
> `juniper-ml/notes/JUNIPER_CONDA_ENV_REBUILD_PROCEDURE.md`) creates a new
> `juniper-ml/notes/JUNIPER_2026-05-03_JUNIPER-ECOSYSTEM_CONDA-ENV-REBUILD-PROCEDURE.md`) creates a new
> suffixed environment (`JuniperCanopy1` → `JuniperCanopy2` → …) and renames the
> previous one to `*-DEPRECATED`. A `*-DEPRECATED` environment has a broken or
> partial toolchain (e.g. a broken `torch` import) — **never activate one**.
Expand Down
2 changes: 1 addition & 1 deletion notes/FRONTEND_ISSUES_PLAN_2026-05-09.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* **Author**: Paul Calnon (drafted by Claude Code Opus 4.7)
* **Status**: Reviewed — open questions resolved 2026-05-09 (see §10 Resolution log)
* **Scope**: Six user-reported issues affecting the juniper-canopy Dash UI and its interaction with the juniper-cascor backend.
* **Companion pointer**: `juniper-ml/notes/canopy_frontend_issues_plan_2026-05-09.md`
* **Companion pointer**: `juniper-ml/notes/JUNIPER_2026-05-09_JUNIPER-CANOPY_FRONTEND-ISSUES-PLAN.md`

## Revisions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ But that handler runs only **after** the middleware admits the request. The midd

| Doc | Relevance |
| --- | --- |
| `juniper-ml/notes/observability/CANOPY_DASHBOARD_SELF_CALL_REFACTOR_2026-05-10.md` | **Most relevant.** Docs Opt-B (X-API-Key injection in **server-side** self-calls, shipped canopy#265) vs deferred Opt-C (in-process calls). Writes why **browser-originated** calls (this bug) were never covered. Cross-repo. |
| `juniper-ml/notes/observability/JUNIPER_2026-05-10_JUNIPER-CANOPY_DASHBOARD-SELF-CALL-REFACTOR.md` | **Most relevant.** Docs Opt-B (X-API-Key injection in **server-side** self-calls, shipped canopy#265) vs deferred Opt-C (in-process calls). Writes why **browser-originated** calls (this bug) were never covered. Cross-repo. |
| `notes/CANOPY_TRAINING_CONTROL_ERROR_SURFACING_DESIGN_2026-06-14.md` | Designs the "`{label} failed.`" danger-alert surfacing that renders this 401; both transports feed `training-control-action`. Explains the banner mechanism (not the auth cause). |
| `notes/fixes/FIX_FRONTEND_REGRESSIONS_2026-05-30.md` | Frontend-regression remediation observed on the **deployed docker stack** (Bug-4 class, canopy↔cascor); same deploy context as this failure. |
| `notes/CANOPY_RUNTIME_CLIENT_FLOOR_DRIFT_ROOT_CAUSE_2026-06-26.md` | Prior "green tests / dead app" root-cause writeup (client-wheel floor drift). Different root cause, but the same "tests pass, runtime breaks" methodology applies. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
**Scope**: Records the two now-enforced control-surface invariants shipped in this PR and, explicitly, what remains deferred.

> Design of record (read first): juniper-ml
> [`notes/JUNIPER_CANOPY_CONTROL_SURFACE_AUTH_AND_NAT_DESIGN_2026-07-03.md`](https://github.com/pcalnon/juniper-ml/blob/main/notes/JUNIPER_CANOPY_CONTROL_SURFACE_AUTH_AND_NAT_DESIGN_2026-07-03.md)
> [`notes/JUNIPER_CANOPY_CONTROL_SURFACE_AUTH_AND_NAT_DESIGN_2026-07-03.md`](https://github.com/pcalnon/juniper-ml/blob/main/notes/JUNIPER_2026-07-03_JUNIPER-CANOPY_CONTROL-SURFACE-AUTH-AND-NAT-DESIGN.md)
> — §4 (SEC-F22 Option A), §5 (SEC-F19 Option B), §7 Phases 1–2, §8 decisions D2 + D4, §9 testing.
> Companion: [`JUNIPER_CANOPY_TRAINING-CONTROL-AUTH_DESIGN_2026-06-30.md`](JUNIPER_CANOPY_TRAINING-CONTROL-AUTH_DESIGN_2026-06-30.md)
> §7.3 stated the load-bearing loopback precondition this PR now enforces.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ banner via `_surface_training_control_outcome_handler` (`src/frontend/dashboard_
| C5 | **`/v1/*` and other non-browser API surfaces must remain key-gated.** External / programmatic clients can and do hold a key. | `/v1/*` is not exempt (`src/canopy_constants.py:355-367`); the audit confirms live `GET /v1/` → 401. |

**Non-goals.** (a) Option C (in-process self-calls) from
`juniper-ml/notes/observability/CANOPY_DASHBOARD_SELF_CALL_REFACTOR_2026-05-10.md`
`juniper-ml/notes/observability/JUNIPER_2026-05-10_JUNIPER-CANOPY_DASHBOARD-SELF-CALL-REFACTOR.md`
— it addresses the *server-side* self-call cost, not the *browser* path, and does not fix this 401. (b) Enabling the SEC-06 bearer subprotocol
(`ws_auth_enabled`, `src/settings.py:341`) — it still requires the token *in the browser* (§4d). (c) Re-architecting the dashboard to a separate
process. (d) Changing the secret's value or rotation.
Expand Down Expand Up @@ -504,7 +504,7 @@ curl -sS -i http://localhost:8050/v1/health # expect 200
**Companion / cross-repo:**

- [`notes/JUNIPER_CANOPY_CASCOR-TRAINING-401-APIKEY_AUDIT_2026-06-29.md`](JUNIPER_CANOPY_CASCOR-TRAINING-401-APIKEY_AUDIT_2026-06-29.md) — the root-cause audit (H1/H1b, the toggle table, the call path).
- `juniper-ml/notes/observability/CANOPY_DASHBOARD_SELF_CALL_REFACTOR_2026-05-10.md` — Option B (server-side `X-API-Key` injection, shipped canopy#265) vs deferred Option C; §7 explicitly notes browser-side data fetching as out of scope, which is why browser-originated calls were never covered.
- `juniper-ml/notes/observability/JUNIPER_2026-05-10_JUNIPER-CANOPY_DASHBOARD-SELF-CALL-REFACTOR.md` — Option B (server-side `X-API-Key` injection, shipped canopy#265) vs deferred Option C; §7 explicitly notes browser-side data fetching as out of scope, which is why browser-originated calls were never covered.
- [`notes/CANOPY_TRAINING_CONTROL_ERROR_SURFACING_DESIGN_2026-06-14.md`](CANOPY_TRAINING_CONTROL_ERROR_SURFACING_DESIGN_2026-06-14.md) — the danger-alert surfacing that renders this 401 (both transports feed `training-control-action`).
- juniper-deploy `/home/pcalnon/Development/python/Juniper/juniper-deploy/docker-compose.yml` — canopy service (`:545`), `CANOPY_API_KEY_FILE` (`:576`), loopback bind (`:557`), rate limit on (`:572`), `secrets: - canopy_api_key` (`:616-617`), secret def (`:932-933`); cascor WS origins (`:234,:384`, **not** canopy inbound).
- juniper-deploy `/home/pcalnon/Development/python/Juniper/juniper-deploy/.env.secrets.enc:10` (encrypted `CANOPY_API_KEY`), `.env.secrets.example:13` (empty template), `./secrets/canopy_api_key.txt` (43 bytes).
2 changes: 1 addition & 1 deletion notes/development/CONSOLIDATED_DEVELOPMENT_HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ Three files in `notes/development/` are broken symlinks whose targets no longer

| File | Target | Status |
|---------------------------------------|-----------------------------------------------------------------------------|-------------------|
| `DASHBOARD_AUGMENTATION_PLAN.md` | `../../../juniper-ml/notes/DASHBOARD_AUGMENTATION_PLAN.md` | ⚠️ Target missing |
| `DASHBOARD_AUGMENTATION_PLAN.md` | `../../../juniper-ml/notes/JUNIPER_2026-04-20_JUNIPER-CANOPY_DASHBOARD-AUGMENTATION-PLAN.md` | ⚠️ Target missing |
| `DATASET_DISPLAY_FAILURE_ANALYSIS.md` | `../../../juniper-ml/notes/development/DATASET_DISPLAY_FAILURE_ANALYSIS.md` | ⚠️ Target missing |
| `DATASET_DISPLAY_FIX_PLAN.md` | `../../../juniper-ml/notes/development/DATASET_DISPLAY_FIX_PLAN.md` | ⚠️ Target missing |

Expand Down
2 changes: 1 addition & 1 deletion notes/development/REPLAY_V2_FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ why decision-boundary playback "lags" the metric scrubber.

For implementation roadmap and design rationale see the parent plan
at
[`juniper-ml/notes/PHASE_6E_DEFERRED_CAN-015GH_DESIGN.md`](https://github.com/pcalnon/juniper-ml/blob/main/notes/PHASE_6E_DEFERRED_CAN-015GH_DESIGN.md).
[`juniper-ml/notes/JUNIPER_2026-05-04_JUNIPER-ECOSYSTEM_PHASE-6E-DEFERRED-CAN-015GH-DESIGN.md`](https://github.com/pcalnon/juniper-ml/blob/main/notes/JUNIPER_2026-05-04_JUNIPER-ECOSYSTEM_PHASE-6E-DEFERRED-CAN-015GH-DESIGN.md).

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Date**: 2026-04-12
**Version Reviewed**: 0.4.0
**Source of Truth**: [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/R5-01_canonical_development_plan.md)
**Source of Truth**: [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_R5-01-CANONICAL-DEVELOPMENT-PLAN.md)
**Supersedes**: [CODE_REVIEW_ANALYSIS_2026-04-04.md](CODE_REVIEW_ANALYSIS_2026-04-04.md)

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Date**: 2026-04-12
**Version**: 0.4.0
**Source of Truth**: [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/R5-01_canonical_development_plan.md)
**Source of Truth**: [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_R5-01-CANONICAL-DEVELOPMENT-PLAN.md)
**Supersedes**: [CODE_REVIEW_AUDIT_PLAN_2026-04-12.md](CODE_REVIEW_AUDIT_PLAN_2026-04-12.md)

**Companion Documents**:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Date**: 2026-04-12
**Version**: 0.4.0
**Source of Truth**: [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/R5-01_canonical_development_plan.md)
**Source of Truth**: [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_R5-01-CANONICAL-DEVELOPMENT-PLAN.md)
**Supersedes**: [CODE_REVIEW_DEVELOPMENT_ROADMAP_2026-04-04.md](CODE_REVIEW_DEVELOPMENT_ROADMAP_2026-04-04.md)

**Companion Documents**:
Expand Down Expand Up @@ -154,7 +154,7 @@ Track PAR is otherwise complete.

## 4. R5-01 Canonical Phase Execution

This section summarizes the 11 R5-01 phases and their canopy impact. See [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/R5-01_canonical_development_plan.md) for full detail.
This section summarizes the 11 R5-01 phases and their canopy impact. See [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_R5-01-CANONICAL-DEVELOPMENT-PLAN.md) for full detail.

### Phase 0-cascor: Seq, Replay, Resume (2.0 days)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Date**: 2026-04-12
**Version**: 0.4.0
**Source of Truth**: [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/R5-01_canonical_development_plan.md)
**Source of Truth**: [R5-01 Canonical Development Plan](../../juniper-ml/notes/interface_proposals/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_R5-01-CANONICAL-DEVELOPMENT-PLAN.md)
**Companion**: [CODE_REVIEW_ANALYSIS_2026-04-12_R5-01-aligned.md](CODE_REVIEW_ANALYSIS_2026-04-12_R5-01-aligned.md)
**Supersedes**: [CODE_REVIEW_PLAN_2026-04-04.md](CODE_REVIEW_PLAN_2026-04-04.md)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ grep -n 'enable_ws_control_buttons' src/settings.py # = True (src/settings.py
## Reference docs

- [`notes/JUNIPER_CANOPY_CASCOR-TRAINING-401-APIKEY_AUDIT_2026-06-29.md`](../../notes/JUNIPER_CANOPY_CASCOR-TRAINING-401-APIKEY_AUDIT_2026-06-29.md) — **primary source**: full ranked root-cause, toggle table, call-path.
- `juniper-ml/notes/observability/CANOPY_DASHBOARD_SELF_CALL_REFACTOR_2026-05-10.md` (cross-repo) — **most relevant**: Option B (server-side `X-API-Key` injection, shipped canopy#265) vs deferred Option C; explains why **browser-originated** calls were never covered.
- `juniper-ml/notes/observability/JUNIPER_2026-05-10_JUNIPER-CANOPY_DASHBOARD-SELF-CALL-REFACTOR.md` (cross-repo) — **most relevant**: Option B (server-side `X-API-Key` injection, shipped canopy#265) vs deferred Option C; explains why **browser-originated** calls were never covered.
- [`src/frontend/internal_api.py`](../../src/frontend/internal_api.py) (module docstring) — states the helper is for **server-side** self-calls only; points to the refactor doc above.
- [`notes/CANOPY_TRAINING_CONTROL_ERROR_SURFACING_DESIGN_2026-06-14.md`](../../notes/CANOPY_TRAINING_CONTROL_ERROR_SURFACING_DESIGN_2026-06-14.md) — designs the "`{label} failed.`" danger-alert that renders this 401 (the banner mechanism, not the auth cause).
- [`notes/fixes/FIX_FRONTEND_REGRESSIONS_2026-05-30.md`](../../notes/fixes/FIX_FRONTEND_REGRESSIONS_2026-05-30.md) — frontend-regression remediation on the **deployed docker stack** (same deploy context).
Expand Down
2 changes: 1 addition & 1 deletion src/backend/demo_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
#
#####################################################################################################################################################################################################
# References:
# - juniper-ml/notes/MICROSERVICES-ARCHITECTURE_DEVELOPMENT-ROADMAP.md §5.5
# - juniper-ml/notes/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_MICROSERVICES-ARCHITECTURE-DEVELOPMENT-ROADMAP.md §5.5
#
#####################################################################################################################################################################################################
# TODO :
Expand Down
4 changes: 2 additions & 2 deletions src/backend/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
#####################################################################################################################################################################################################
# Notes:
# Phase 5 of the Microservices Architecture Development Roadmap.
# See: juniper-ml/notes/MICROSERVICES-ARCHITECTURE_DEVELOPMENT-ROADMAP.md
# See: juniper-ml/notes/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_MICROSERVICES-ARCHITECTURE-DEVELOPMENT-ROADMAP.md
#
#####################################################################################################################################################################################################
# References:
# - PEP 544 — Protocols: Structural subtyping (static duck typing)
# - juniper-ml/notes/MICROSERVICES-ARCHITECTURE_DEVELOPMENT-ROADMAP.md §5.4
# - juniper-ml/notes/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_MICROSERVICES-ARCHITECTURE-DEVELOPMENT-ROADMAP.md §5.4
#
#####################################################################################################################################################################################################
# TODO :
Expand Down
2 changes: 1 addition & 1 deletion src/backend/service_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
#
#####################################################################################################################################################################################################
# References:
# - juniper-ml/notes/MICROSERVICES-ARCHITECTURE_DEVELOPMENT-ROADMAP.md §5.6
# - juniper-ml/notes/JUNIPER_2026-04-20_JUNIPER-ECOSYSTEM_MICROSERVICES-ARCHITECTURE-DEVELOPMENT-ROADMAP.md §5.6
#
#####################################################################################################################################################################################################
# TODO :
Expand Down
2 changes: 1 addition & 1 deletion src/demo_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def __init__(self, update_interval: float = None):

# Phase 3 progress fields (mirror cascor TrainingState semantics so the
# canopy progress UI displays non-zero values during demo training).
# See juniper-ml/notes/code-review/CANOPY_CASCOR_INTERFACE_ROADMAP_2026-04-08.md §5.
# See juniper-ml/notes/code-review/JUNIPER_2026-04-08_JUNIPER-ECOSYSTEM_CANOPY-CASCOR-INTERFACE-ROADMAP.md §5.
self._best_correlation_state: float = 0.0
self._candidates_trained_count: int = 0
self._candidates_total_count: int = 0
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/internal_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
``self._api_base_url`` URLs.

Long-term direction (Option C, deferred — see
juniper-ml/notes/observability/CANOPY_DASHBOARD_SELF_CALL_REFACTOR_2026-05-10.md):
juniper-ml/notes/observability/JUNIPER_2026-05-10_JUNIPER-CANOPY_DASHBOARD-SELF-CALL-REFACTOR.md):
the HTTP self-call indirection should be replaced with direct in-process
function calls into the FastAPI route handlers, eliminating the
serialize/deserialize round-trip and the metric-noise contribution. This
Expand Down
2 changes: 1 addition & 1 deletion src/tests/ui/test_l3_native_setter_poc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
native-value-setter does NOT drive Dash's React-controlled ``dbc.Input(type=number)``.

This is the reproducible artifact behind the audit doc's §5.3 / §6 claim
(`juniper-ml/notes/JUNIPER_CANOPY_AUDIT_REGRESSIONS_AND_MODEL_SELECTION_2026-06-15.md`).
(`juniper-ml/notes/JUNIPER_2026-06-15_JUNIPER-CANOPY_AUDIT-REGRESSIONS-AND-MODEL-SELECTION.md`).
It is a **strict xfail**: the assertion that the set value reaches the backend
is expected to FAIL (Apply pushes the *default*, not the set value), proving the
POC #2 wall empirically and continuously. If a future Dash/dbc release fixes the
Expand Down
2 changes: 1 addition & 1 deletion src/tests/unit/test_demo_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ class TestPhase4TypedContract:
"""Phase 4 / P5-RC-18: DemoBackend conforms to the same TypedDict
contract as ServiceBackend. Field-presence assertions only — TypedDicts
erase to plain dicts at runtime.
See juniper-ml/notes/code-review/CANOPY_CASCOR_INTERFACE_ROADMAP_2026-04-08.md §6.1.
See juniper-ml/notes/code-review/JUNIPER_2026-04-08_JUNIPER-ECOSYSTEM_CANOPY-CASCOR-INTERFACE-ROADMAP.md §6.1.
"""

def test_apply_params_returns_envelope(self, demo_backend):
Expand Down
2 changes: 1 addition & 1 deletion src/tests/unit/test_demo_mode_advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def test_cascade_unit_addition(self):
class TestPhase3ProgressFields:
"""Phase 3 metrics granularity: TrainingState progress fields are exposed
by DemoMode so the canopy progress UI displays non-zero values during demo
training. See juniper-ml/notes/code-review/CANOPY_CASCOR_INTERFACE_ROADMAP_2026-04-08.md §5.
training. See juniper-ml/notes/code-review/JUNIPER_2026-04-08_JUNIPER-ECOSYSTEM_CANOPY-CASCOR-INTERFACE-ROADMAP.md §5.
"""

PHASE3_FIELDS = (
Expand Down
2 changes: 1 addition & 1 deletion src/tests/unit/test_service_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ class TestPhase4TypedContract:
"""Phase 4 / P5-RC-18: BackendProtocol method returns conform to the
declared TypedDict shapes. These are field-presence contract tests, not
runtime type assertions — TypedDicts erase to plain dicts at runtime.
See juniper-ml/notes/code-review/CANOPY_CASCOR_INTERFACE_ROADMAP_2026-04-08.md §6.1.
See juniper-ml/notes/code-review/JUNIPER_2026-04-08_JUNIPER-ECOSYSTEM_CANOPY-CASCOR-INTERFACE-ROADMAP.md §6.1.
"""

def test_start_training_envelope_keys(self, service_backend):
Expand Down