# WEPP Output Scope Contract
> Authoritative contract for report `output_scope` selection and baseline-vs-roads output isolation.
> **See also:** `docs/schemas/rq-response-contract.md`, `docs/schemas/weppcloud-browse-parquet-filter-contract.md`, `wepppy/nodb/mods/roads/specification.md`

## Normative Status
- This document is normative and authoritative for `output_scope` behavior.
- Requirement keywords `MUST`, `MUST NOT`, `SHOULD`, and `MAY` are interpreted per RFC 2119.
- If implementation and this contract diverge, either:
  - implementation MUST be corrected, or
  - this contract MUST be updated in the same change set.

## Scope
- Defines canonical `output_scope` values and normalization.
- Defines dataset path mapping and run-output isolation rules.
- Defines route-level `output_scope` support and invalid-input behavior.
- Defines Runs-page link discovery rules for Roads run results.
- Does not replace auth/capability requirements for those routes.

## Canonical Values
- Allowed values: `baseline | roads`.
- Default when query parameter is missing or empty: `baseline`.
- Value matching is case-insensitive (`ROADS` normalizes to `roads`).
- Invalid values MUST fail explicitly; silent fallback is forbidden.

Implementation reference:
- `wepppy/wepp/reports/output_scope.py::normalize_output_scope`

## Path Mapping Contract
- Baseline output root: `wepp/output`
- Roads output root: `wepp/roads/output`
- Interchange root: `<output_root>/interchange`

`scoped_dataset_path(...)` rules:
- For `baseline`, return the input path unchanged.
- For `roads`, rewrite only paths rooted at `wepp/output` to `wepp/roads/output`.
- Paths outside `wepp/output` MUST remain unchanged.

Examples:
- `wepp/output/interchange/H.wat.parquet` + `roads` -> `wepp/roads/output/interchange/H.wat.parquet`
- `watershed/hillslopes.parquet` + `roads` -> unchanged

Watershed-table schema note:
- `output_scope` rewrites do not alter watershed table schemas.
- `watershed/hillslopes.parquet` may include additive Peridot provenance columns such as
  `length_estimate_mode`, `length_area_over_channel`, and `length_edge_median`; scoped
  path behavior remains unchanged. Direct Peridot output behavior is documented in the
  [Peridot watershed output contract](https://github.com/wepp-in-the-woods/peridot/blob/main/docs/contracts/watershed-output-contract.md).

Implementation references:
- `wepppy/wepp/reports/output_scope.py::resolve_output_scope_paths`
- `wepppy/wepp/reports/output_scope.py::scoped_dataset_path`

## Storage Isolation Contract
- Roads-scoped report resource regeneration MUST write only under `wepp/roads/output/interchange/*`.
- Roads flows MUST NOT mutate baseline `wepp/output/*`.
- Required Roads resources MUST be checked after regeneration; missing required resources MUST fail explicitly with `FileNotFoundError`.
- Successful regeneration persists `roads_report_resources.output_scope = "roads"`.

Implementation reference:
- `wepppy/nodb/mods/roads/roads.py::_regenerate_roads_report_resources`

### AgFields routing-suite boundary

AgFields routing results are independently composable experimental trees under
`wepp/ag_fields/watershed/{concept-1,concept-2,hybrid}/`. They are not canonical
`output_scope` values and MUST NOT be selected by passing their identifiers to a
standard WEPP report route. Their runs, outputs, manifests, and interchange files
remain isolated from both `wepp/output` and `wepp/roads/output`; current UI support
is authenticated execution/state plus direct artifact browsing. Making a standard
report scheme-aware requires an explicit additive contract and route tests in a
future change.

## Route Support Matrix
| Route | `output_scope` Query Param | Default | Invalid Value Behavior | Notes |
| --- | --- | --- | --- | --- |
| `/runs/<runid>/<config>/report/wepp/summary` | Yes | `baseline` | `400` + JSON error payload | Scope passed to outlet/hill/channel summary reports. |
| `/runs/<runid>/<config>/report/wepp/yearly_watbal` | Yes | `baseline` | `400` + JSON error payload | Scope passed to `TotalWatbalReport`. |
| `/runs/<runid>/<config>/report/wepp/avg_annual_watbal` | Yes | `baseline` | `400` + JSON error payload | Scope passed to hillslope/channel watbal reports. |
| `/runs/<runid>/<config>/plot/wepp/streamflow` | Yes | `baseline` | `400` + JSON error payload | Uses scope-aware `totalwatsed3.parquet` relpath. |
| `/runs/<runid>/<config>/report/wepp/return_periods` | Yes | `baseline` | `400` + JSON error payload | Scope passed to return-period postprocess service. |
| `/runs/<runid>/<config>/gl-dashboard` | Yes | `baseline` | `400` + JSON error payload | Scope-aware WEPP dataset relpaths injected into template context. |
| `/runs/<runid>/<config>/storm-event-analyzer` | Yes | `baseline` | `400` + JSON error payload | Scope-aware WEPP dataset relpaths injected into template context. |
| `/runs/<runid>/<config>/report/wepp/results` | No | N/A | N/A | Baseline Run Results panel. |
| `/runs/<runid>/<config>/report/roads/results` | No | N/A | N/A | Roads Run Results panel; links are generated as roads-scoped where applicable. |

## Runs-Page Trigger and Discovery Contract
### Baseline WEPP Run Results panel
- Served by `/report/wepp/results`.
- Link set is template-driven (`controls/wepp_reports.htm`) with conditional visibility for baseline artifacts:
  - `totalwatsed3.parquet` and `totalwatsed2.parquet` download links appear only when those files exist.
  - interchange README link appears only when `wepp/output/interchange/README.md` exists.

### Roads Run Results panel
- Served by `/report/roads/results` and rendered from `controls/roads_reports.htm`.
- Panel visibility condition:
  - `run_summary_state == "completed"`
  - `roads_report_resources.status == "ready"`
  - `roads_report_resources.missing_relpaths` is empty
- Links are discovered from `roads_report_resources` and emitted by `_roads_run_results_report_links(...)`.

Roads link gating by required resources:
- `Watershed Loss Summary`: requires `loss_pw0.out.parquet`, `loss_pw0.hill.parquet`, `loss_pw0.chn.parquet`.
- `Return Periods`: requires `ebe_pw0.parquet`, `totalwatsed3.parquet`.
- `Yearly Water Balance`: requires `totalwatsed3.parquet`.
- `Daily Streamflow`: requires `totalwatsed3.parquet`.
- `Average Annual Water Balance`: requires `H.wat.parquet`.
- `GL Dashboard` and `Storm Event Analyzer`: included in Roads link set when the panel is ready.
- `Road Segment Loss Summary (Parquet/CSV)`: requires non-empty `roads_segment_loss_summary_relpath`.

Client trigger behavior:
- Roads controller JS fetches `/report/roads/results/` on initialization and after prepare/run completion to refresh the panel in place.

Implementation references:
- `wepppy/weppcloud/routes/nodb_api/wepp_bp.py::report_wepp_results`
- `wepppy/weppcloud/routes/nodb_api/roads_bp.py::_roads_run_results_report_links`
- `wepppy/weppcloud/templates/controls/wepp_reports.htm`
- `wepppy/weppcloud/templates/controls/roads_reports.htm`
- `wepppy/weppcloud/controllers_js/roads.js::renderRoadsResultsPanel`

## Format Contract
- Canonical persisted report resources in scoped output directories SHOULD be Parquet.
- `roads_segment_loss_summary` MUST be persisted only as:
  - `wepp/roads/output/interchange/roads_segment_loss_summary.parquet`
- CSV export MUST be runtime conversion from Parquet via:
  - `/download/{subpath}?as_csv=1`
- No on-disk CSV shadow artifact is allowed for roads segment loss summary.

## Error Contract
- Library-level scope validation (`normalize_output_scope`) raises:
  - `ValueError("Invalid output_scope '<value>'. Expected one of: baseline|roads.")`
- Route-level invalid scope handling MUST return:
  - HTTP `400`
  - JSON payload containing `error.message` with the invalid-scope text.
- Roads resource regeneration missing required artifacts MUST raise `FileNotFoundError` with missing relpaths listed.

## Compliance Checklist
When adding or modifying scope-aware report surfaces:
1. Validate `output_scope` using `normalize_output_scope`.
2. Use `scoped_dataset_path` or `resolve_output_scope_paths` for all scoped dataset paths.
3. Preserve `output_scope` in rendered template links/forms/selectors.
4. Add/maintain route tests for:
   - default scope behavior,
   - `output_scope=roads`,
   - invalid scope (`400`).
5. Keep roads-only writes under `wepp/roads/output/*`; never mutate `wepp/output/*` in roads flows.
6. Update this contract in the same change set when scope behavior changes.
