diff --git a/.github/workflows/validate-public-mirror.yml b/.github/workflows/validate-public-mirror.yml
new file mode 100644
index 0000000..9def981
--- /dev/null
+++ b/.github/workflows/validate-public-mirror.yml
@@ -0,0 +1,38 @@
+name: Validate Public Mirror
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ validate-public-mirror:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Run cross-platform public mirror validator
+ shell: pwsh
+ run: |
+ python scripts/validate-public-mirror.py *> public-mirror-validation.log
+ $exitCode = $LASTEXITCODE
+ Get-Content public-mirror-validation.log
+ if ($exitCode -ne 0) {
+ Write-Error "Public mirror validation failed with exit code $exitCode"
+ exit $exitCode
+ }
+ - name: Upload public mirror validation log
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: public-mirror-validation-log
+ path: public-mirror-validation.log
diff --git a/README.md b/README.md
index b4f7290..20bbc4c 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,8 @@ Clear Lake Watch is a late-prototype / early-MVP public dashboard for organizing
It is not official public-health guidance, an official advisory, a validated forecast, or a deployed sensor network.
+This public mirror is a static reviewed snapshot generated on May 5, 2026, and it is not live lake conditions.
+
**Live public mirror:** [coreytshaffer.github.io/clear-lake-watch](https://coreytshaffer.github.io/clear-lake-watch/)

@@ -19,6 +21,8 @@ It is not official public-health guidance, an official advisory, a validated for
- Private/local records, reviewer notes, raw field records, and trusted-review paths are excluded from the public mirror.
- Dashboard is not official guidance, a health advisory, a validated forecast, or a deployed sensor network.
+See [`docs/epistemic_safety_case.md`](docs/epistemic_safety_case.md) for the project’s reviewer-facing explanation of freshness, provenance, warning-preservation, and fail-closed publication boundaries.
+
## Reviewer Start Here
For a 5-minute review:
diff --git a/app.js b/app.js
index 767b34d..dcce048 100644
--- a/app.js
+++ b/app.js
@@ -23,6 +23,7 @@ const mapReviewStatusElement = document.querySelector("#map-review-status");
const siteReviewGridElement = document.querySelector("#site-review-grid");
const productGridElement = document.querySelector("#product-grid");
const sourceStatusGridElement = document.querySelector("#source-status-grid");
+const sourceFreshnessLegendElement = document.querySelector("#source-freshness-legend");
const sourceOutputGridElement = document.querySelector("#source-output-grid");
const manifestNotesElement = document.querySelector("#manifest-notes");
const weatherContextGridElement = document.querySelector("#weather-context-grid");
@@ -1436,11 +1437,17 @@ const renderDataProducts = (products = []) => {
};
const renderSourceStatus = (manifestData) => {
- if (!sourceStatusGridElement && !sourceOutputGridElement && !manifestNotesElement) {
+ if (
+ !sourceStatusGridElement &&
+ !sourceFreshnessLegendElement &&
+ !sourceOutputGridElement &&
+ !manifestNotesElement
+ ) {
return;
}
sourceStatusGridElement?.replaceChildren();
+ sourceFreshnessLegendElement?.replaceChildren();
sourceOutputGridElement?.replaceChildren();
manifestNotesElement?.replaceChildren();
@@ -1453,6 +1460,29 @@ const renderSourceStatus = (manifestData) => {
return;
}
+ const freshnessLegend = Array.isArray(manifestData.freshnessLegend)
+ ? manifestData.freshnessLegend
+ : [];
+
+ freshnessLegend.forEach((entry) => {
+ const item = document.createElement("article");
+ item.className = "source-freshness-item";
+
+ const term = document.createElement("h4");
+ term.textContent = entry.term ?? "Freshness field";
+
+ const field = document.createElement("p");
+ field.className = "source-status-meta";
+ field.textContent = entry.field ? `Manifest field: ${entry.field}` : "Manifest field: not specified";
+
+ const definition = document.createElement("p");
+ definition.className = "source-status-note";
+ definition.textContent = entry.definition ?? "Freshness meaning is not defined in the manifest.";
+
+ item.append(term, field, definition);
+ sourceFreshnessLegendElement?.append(item);
+ });
+
manifestData.sources.forEach((source) => {
const card = document.createElement("article");
const status = source.status ?? "unknown";
@@ -1487,7 +1517,7 @@ const renderSourceStatus = (manifestData) => {
freshness.textContent =
source.resourceAgeDays === null || source.resourceAgeDays === undefined
? source.note
- : `Resource file age: ${source.resourceAgeDays} days. ${source.note}`;
+ : `Resource freshness: source file was ${source.resourceAgeDays} days old at refresh. Observation freshness is listed separately above. ${source.note}`;
card.append(title, statusBadge, meta, date, freshness);
sourceStatusGridElement?.append(card);
@@ -1590,7 +1620,7 @@ const renderSnapshotStatusStrip = (manifestData) => {
value: fhabsReports?.latestObservationDate
? formatDate(fhabsReports.latestObservationDate)
: "Unavailable",
- note: "Report dates reflect what is present in the public FHABS source file.",
+ note: "Observation freshness from FHABS report records; this is separate from source-file freshness.",
kind: "reported",
},
{
@@ -1598,7 +1628,7 @@ const renderSnapshotStatusStrip = (manifestData) => {
value: fhabsResults?.latestObservationDate
? formatDate(fhabsResults.latestObservationDate)
: "Unavailable",
- note: "Lab-linked result records may lag the public report stream.",
+ note: "Observation freshness from lab-linked records; source-resource age is tracked separately.",
kind: "reported",
},
{
@@ -1643,7 +1673,7 @@ const renderFreshnessBadge = (generatedAt, { unavailable = false } = {}) => {
note.className = "freshness-note";
note.textContent = unavailable
? "The public data bundle could not be loaded, so snapshot values are not being shown."
- : "Source observation dates may be older than the dashboard refresh time.";
+ : "Observation freshness can be older than dashboard refresh time; resource freshness only says whether the source file was recently accessible.";
freshnessRowElement.append(badge, note);
};
diff --git a/data/manifest.json b/data/manifest.json
index a941303..ab5ae5c 100644
--- a/data/manifest.json
+++ b/data/manifest.json
@@ -1,90 +1,116 @@
{
- "generatedAt": "2026-05-05T01:44:50.0255703-07:00",
- "schemaVersion": 1,
- "dashboard": "Clear Lake Watch",
- "status": "ok",
- "sourceFreshnessMaxAgeDays": 14,
- "sources": [
- {
- "id": "usgs-lake-level",
- "label": "USGS Lakeport lake level",
- "source": "USGS",
- "status": "ok",
- "rowCount": 29,
- "latestObservationDate": "2026-05-03",
- "station": "11450000",
- "parameter": "00065",
- "note": "USGS daily values used for the Lakeport lake-level card and hydrology series."
- },
- {
- "id": "usgs-cole-creek-discharge",
- "label": "USGS Cole Creek discharge",
- "source": "USGS",
- "status": "ok",
- "rowCount": 29,
- "latestObservationDate": "2026-05-03",
- "station": "11449820",
- "parameter": "00060",
- "note": "USGS daily values used for tributary-flow context and hydrology series."
- },
- {
- "id": "fhabs-bloom-reports",
- "label": "FHABS bloom reports",
- "source": "California Water Boards FHABS",
- "status": "ok",
- "rowCount": 3429,
- "clearLakeRowCount": 230,
- "latestObservationDate": "2025-09-07",
- "resourceUrl": "https://data.ca.gov/dataset/ab672540-aecd-42f1-9b05-9aad326f97ec/resource/c6a36b91-ad38-4611-8750-87ee99e497dd/download/bloom-report_2026-05-04.csv",
- "resourceDate": "2026-05-04",
- "resourceAgeDays": 1,
- "note": "Public report records filtered to Clear Lake and normalized into reports, map markers, arm summaries, and report-pattern analytics."
- },
- {
- "id": "fhabs-results",
- "label": "FHABS lab results",
- "source": "California Water Boards FHABS",
- "status": "ok",
- "rowCount": 2080,
- "clearLakeRowCount": 169,
- "latestObservationDate": "2024-01-11",
- "resourceUrl": "https://data.ca.gov/dataset/ab672540-aecd-42f1-9b05-9aad326f97ec/resource/9d4e1df4-0cd6-4165-9e63-effcafd9dccc/download/hab-results_2026-05-04.csv",
- "resourceDate": "2026-05-04",
- "resourceAgeDays": 1,
- "note": "Public lab-linked result records filtered to Clear Lake and normalized into observation-shaped records."
- }
- ],
- "outputs": [
- {
- "file": "data/live.json",
- "recordCount": 13,
- "description": "Dashboard live snapshot cards, map markers, analytics bundle, and data product index."
- },
- {
- "file": "data/reports.json",
- "recordCount": 230,
- "description": "Normalized FHABS Clear Lake report records."
- },
- {
- "file": "data/observations.json",
- "recordCount": 62,
- "description": "Normalized USGS and FHABS observation records."
- },
- {
- "file": "data/sites-normalized.json",
- "recordCount": 11,
- "description": "Normalized stable site registry export."
- },
- {
- "file": "data/analytics.json",
- "recordCount": 20,
- "description": "Precomputed reporting-pattern and observation-coverage analytics."
- }
- ],
- "notes": [
- "Observation dates may be older than the dashboard generation time.",
- "Lakeport lake-level values are shown as feet Rumsey, with approximate elevation calculated using Zero Rumsey = 1318.256 ft above mean sea level.",
- "FHABS report counts represent reporting activity, not direct bloom intensity.",
- "This manifest describes the generated public-data snapshot and is not official public-health guidance."
- ]
-}
\ No newline at end of file
+ "generatedAt": "2026-05-05T01:44:50.0255703-07:00",
+ "schemaVersion": 1,
+ "dashboard": "Clear Lake Watch",
+ "status": "ok",
+ "sourceFreshnessMaxAgeDays": 14,
+ "sources": [
+ {
+ "id": "usgs-lake-level",
+ "label": "USGS Lakeport lake level",
+ "source": "USGS",
+ "status": "ok",
+ "rowCount": 29,
+ "latestObservationDate": "2026-05-03",
+ "station": "11450000",
+ "parameter": "00065",
+ "note": "USGS daily values used for the Lakeport lake-level card and hydrology series."
+ },
+ {
+ "id": "usgs-cole-creek-discharge",
+ "label": "USGS Cole Creek discharge",
+ "source": "USGS",
+ "status": "ok",
+ "rowCount": 29,
+ "latestObservationDate": "2026-05-03",
+ "station": "11449820",
+ "parameter": "00060",
+ "note": "USGS daily values used for tributary-flow context and hydrology series."
+ },
+ {
+ "id": "fhabs-bloom-reports",
+ "label": "FHABS bloom reports",
+ "source": "California Water Boards FHABS",
+ "status": "ok",
+ "rowCount": 3429,
+ "clearLakeRowCount": 230,
+ "latestObservationDate": "2025-09-07",
+ "resourceUrl": "https://data.ca.gov/dataset/ab672540-aecd-42f1-9b05-9aad326f97ec/resource/c6a36b91-ad38-4611-8750-87ee99e497dd/download/bloom-report_2026-05-04.csv",
+ "resourceDate": "2026-05-04",
+ "resourceAgeDays": 1,
+ "note": "Public report records filtered to Clear Lake and normalized into reports, map markers, arm summaries, and report-pattern analytics."
+ },
+ {
+ "id": "fhabs-results",
+ "label": "FHABS lab results",
+ "source": "California Water Boards FHABS",
+ "status": "ok",
+ "rowCount": 2080,
+ "clearLakeRowCount": 169,
+ "latestObservationDate": "2024-01-11",
+ "resourceUrl": "https://data.ca.gov/dataset/ab672540-aecd-42f1-9b05-9aad326f97ec/resource/9d4e1df4-0cd6-4165-9e63-effcafd9dccc/download/hab-results_2026-05-04.csv",
+ "resourceDate": "2026-05-04",
+ "resourceAgeDays": 1,
+ "note": "Public lab-linked result records filtered to Clear Lake and normalized into observation-shaped records."
+ }
+ ],
+ "outputs": [
+ {
+ "file": "data/live.json",
+ "recordCount": 13,
+ "description": "Dashboard live snapshot cards, map markers, analytics bundle, and data product index."
+ },
+ {
+ "file": "data/reports.json",
+ "recordCount": 230,
+ "description": "Normalized FHABS Clear Lake report records."
+ },
+ {
+ "file": "data/observations.json",
+ "recordCount": 62,
+ "description": "Normalized USGS and FHABS observation records."
+ },
+ {
+ "file": "data/sites-normalized.json",
+ "recordCount": 11,
+ "description": "Normalized stable site registry export."
+ },
+ {
+ "file": "data/analytics.json",
+ "recordCount": 20,
+ "description": "Precomputed reporting-pattern and observation-coverage analytics."
+ }
+ ],
+ "notes": [
+ "Observation dates may be older than the dashboard generation time.",
+ "Resource freshness and observation freshness are separate checks.",
+ "Lakeport lake-level values are shown as feet Rumsey, with approximate elevation calculated using Zero Rumsey = 1318.256 ft above mean sea level.",
+ "FHABS report counts represent reporting activity, not direct bloom intensity.",
+ "This manifest describes the generated public-data snapshot and is not official public-health guidance."
+ ],
+ "fieldDefinitions": {
+ "generatedAt": "Dashboard snapshot generation time. This describes when the public JSON files were built, not when lake conditions were observed.",
+ "sourceFreshnessMaxAgeDays": "Review threshold used by validation to warn when source observations are older than the dashboard freshness window.",
+ "latestObservationDate": "Observation freshness field. This is the newest environmental observation date available for a source within the public snapshot.",
+ "resourceDate": "Resource freshness field for downloadable source files. This is the date embedded in the source file URL or release, not an environmental observation date.",
+ "resourceAgeDays": "Resource freshness field for downloadable source files. This is how old the source file was when the dashboard snapshot was generated.",
+ "status": "Machine-readable source load status for the snapshot refresh pass."
+ },
+ "freshnessLegend": [
+ {
+ "term": "Dashboard snapshot freshness",
+ "field": "generatedAt",
+ "definition": "When Clear Lake Watch generated these public JSON files."
+ },
+ {
+ "term": "Observation freshness",
+ "field": "latestObservationDate",
+ "definition": "How recent the newest environmental record is for a source."
+ },
+ {
+ "term": "Resource freshness",
+ "field": "resourceDate / resourceAgeDays",
+ "definition": "How recent the downloaded source file is, separate from the age of the observations inside it."
+ }
+ ]
+}
diff --git a/docs/architecture_sketch.md b/docs/architecture_sketch.md
new file mode 100644
index 0000000..1987846
--- /dev/null
+++ b/docs/architecture_sketch.md
@@ -0,0 +1,21 @@
+# Architecture Sketch
+
+This sketch is intentionally lightweight. It shows the trust boundary for future sensor data without implying a deployed pipeline.
+
+```mermaid
+flowchart LR
+ A["Field Sensor"] --> B["Local Gateway"]
+ B --> C["MQTT Topic"]
+ C --> D["Intake Validator"]
+ D -->|invalid or broken| E["Quarantine / Dead Letter"]
+ D -->|passes deterministic checks| F["Review Queue"]
+ F -->|approved for internal use| G["Internal Store"]
+ G -->|reviewed public-safe export only| H["Clear Lake Watch Public Layer"]
+```
+
+## Boundary Notes
+
+- The local gateway and MQTT layer are internal transport surfaces.
+- The intake validator is where fail-closed behavior starts.
+- Quarantine and dead-letter paths preserve auditability without turning bad records into public claims.
+- Clear Lake Watch should read only reviewed public-safe exports, not raw sensor traffic.
diff --git a/docs/deployment.md b/docs/deployment.md
index 5322e64..48c9787 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -18,13 +18,9 @@ If a local server is already running on port `4173`, run the full endpoint check
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\validate-dashboard.ps1
```
-If you are intentionally reviewing an older static snapshot for portfolio or archival work, make that choice explicit:
+If you are intentionally reviewing an older static snapshot for portfolio or archival work, make that choice explicit in the release note and README rather than relying on a CLI override. There is no `-AllowStaleSnapshot`-style flag today: the validator warns on stale snapshots but does not block or force-override publication, so the reviewer must decide.
-```powershell
-powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\validate-dashboard.ps1 -SkipHttp -AllowStaleSnapshot
-```
-
-Do not use `-AllowStaleSnapshot` for a fresh public publish unless the stale date is explained in the release notes.
+Do not publish a stale snapshot as if it were fresh. If you are intentionally preserving an older static snapshot, name the snapshot date and the reason in the release notes.
Use `docs/publication-review-checklist.md` before staging, committing, pushing, or promoting the dashboard. That checklist separates local review, private repository work, public mirror updates, and flagship portfolio promotion.
@@ -32,7 +28,7 @@ Current working posture as of May 5, 2026:
- The public snapshot was refreshed locally on May 5, 2026.
- Keep publication as a separate decision from local refresh.
-- Do not use `-AllowStaleSnapshot` for a fresh public publish unless the stale date is explained in the release notes.
+- Do not publish a stale snapshot as fresh; if a static snapshot is preserved intentionally, name its date and reason in the release notes. (No CLI flag currently gates this — it is a manual reviewer decision.)
- Capture a current screenshot before broad portfolio promotion.
## GitHub Pages
diff --git a/docs/epistemic_safety_case.md b/docs/epistemic_safety_case.md
new file mode 100644
index 0000000..446cec3
--- /dev/null
+++ b/docs/epistemic_safety_case.md
@@ -0,0 +1,42 @@
+# Epistemic Safety Case
+
+## Purpose
+Clear Lake Watch is designed as an environmental-data trust pipeline. This prototype explicitly demonstrates **epistemic safety**: when source freshness, provenance, confidence, or publication context is weak, the validator surfaces stale-data warnings and requires manual review before any republish rather than producing a clean-looking but misleading public output. Automated stale-data blocking (a hard publication gate) is a design goal, not yet an implemented control.
+
+## Existing Safety-Relevant Mechanics
+This repository natively embeds trust and provenance safeguards throughout its architecture:
+* `scripts/validate-public-mirror.py`: The build verification script ensures that public deployment bounds are strictly enforced.
+* `assert_manifest_freshness`: Ensures that any published data provides the necessary metadata context.
+* `sourceFreshnessMaxAgeDays` & `freshnessLegend`: Establishes the rules for how stale data can be displayed, ensuring users are never misinformed about the data's recency.
+* `docs/publication-review-checklist.md` & `docs/scheduled-public-refresh-design.md`: Document the rigorous publication gates preventing automated overrides of uncertain environmental data.
+* `docs/site-registry-trust-review-pass-001.md`: Tracks the review state of geographical source markers.
+* `needs-local-review`: A status preventing untrusted markers from being promoted to authoritative without explicit evidence.
+* Stale-data handling today: the validator emits warnings when a snapshot or source observation is older than the freshness threshold, and publication remains a manual, reviewed step. (A `-AllowStaleSnapshot`-style override gate is **proposed / not implemented**; no CLI flag currently blocks or force-overrides stale publication.)
+
+## Environmental Data Boundary Conditions & Pipeline Responses
+
+The following table maps critical epistemic failures to their intended safe system behaviors. The status column distinguishes what the validator enforces today from what is a documented design goal:
+
+| Boundary Condition | Intended Safe Behavior | Status Today |
+| :--- | :--- | :--- |
+| **Stale source** | Preserve stale-source warning | Implemented: validator warns and keeps stale-source language visible |
+| **Missing provenance** | Require local review (e.g. `needs-local-review`) | Implemented: markers stay `needs-local-review` |
+| **Conflicting advisory metadata** | Fail closed | Proposed / not implemented |
+| **Simulated data routed toward public output** | Block public promotion | Partially implemented: reviewed-export field guards + private/local exclusion checks |
+| **Publication confidence too weak** | Surface warnings and require manual review before republish | Implemented as a manual review step; automated stale-data blocking (a `-AllowStaleSnapshot`-style gate) is **proposed / not implemented** |
+
+The validator surfaces stale-data warnings and requires manual review before any republish; automated stale-data blocking is not yet implemented.
+
+## Why Sabotage by Omission Matters
+In environmental and public-interest systems, overt malicious action is rare. The most significant threat is **sabotage by omission**—failing to expose uncertainty, hiding the true age of data, removing caveats, or promoting unreviewed simulated results to look like field evidence. By embedding freshness validation and review gating directly into the pipeline, this prototype guards against silently deceiving the public.
+
+## Non-Claims
+To remain strictly within its prototype boundaries, this project makes the following explicit non-claims:
+* Clear Lake Watch is **not** official public health guidance.
+* It is **not** regulatory monitoring.
+* It is **not** a replacement for California FHABS, Lake County Water Resources, or other agency advisories.
+* It does **not** claim comprehensive water-quality coverage.
+* It is purely a prototype demonstrating reviewable environmental-data trust boundaries.
+
+## Future Work
+Future iterations may explore expanding the `freshnessLegend` to handle varying levels of automated confidence, explicitly testing simulation-bounds for future modeling tasks, and further integrating with the `agent-control-evals` framework to formally score pipeline decisions.
diff --git a/docs/field_sensor_readiness_packet.md b/docs/field_sensor_readiness_packet.md
new file mode 100644
index 0000000..30167c5
--- /dev/null
+++ b/docs/field_sensor_readiness_packet.md
@@ -0,0 +1,199 @@
+# Field Sensor Readiness Packet
+
+**Status:** planning and governance artifact
+**Audience:** operators, reviewers, collaborators, and future field-monitoring contributors
+**Prepared:** 2026-06-25
+
+Clear Lake Watch is a late prototype / early MVP. It is not official public-health guidance, an official advisory, recreation guidance, emergency guidance, a validated forecast, a deployed sensor network, or a live ingestion platform.
+
+This packet defines what future field sensor data would need before it could support Clear Lake Watch or any related public environmental reporting. It is documentation-first on purpose. The goal is to make future sensor work governable before it becomes functional.
+
+## Purpose
+
+- Define the minimum trust boundaries for future sensor observations.
+- Separate raw telemetry from validated and publishable environmental records.
+- Preserve provenance, freshness, uncertainty, and reviewability.
+- Keep the public dashboard fail-closed unless reviewed exports are ready.
+
+## Relationship To Clear Lake Watch
+
+Clear Lake Watch is the reviewed publication mirror, not the operational sensor backbone.
+
+The intended operating path remains:
+
+```text
+sensor or field device -> local gateway -> intake validation -> review or quarantine -> reviewed export -> static public mirror
+```
+
+This packet does not authorize direct sensor-to-dashboard publishing. A sensor reading is not automatically environmental truth. It must pass validation, provenance, freshness, and review checks before public display.
+
+## Data Lifecycle
+
+### 1. Raw Observation
+
+A raw observation is the first structured record received from a device or gateway.
+
+Properties:
+
+- may contain transport errors or stale timestamps
+- may have bad units or impossible values
+- may come from an unknown or misconfigured sensor
+- is not publishable
+
+### 2. Auto-Checked Observation
+
+An auto-checked observation passed deterministic checks such as:
+
+- required fields present
+- timestamp parseable
+- sensor and station IDs known
+- coordinates within expected bounds
+- parameter and unit pair allowed
+- value inside instrument or domain sanity ranges
+
+Auto-checked is still not public approval.
+
+### 3. Human-Review Observation
+
+Some records require a reviewer before any downstream use.
+
+Examples:
+
+- borderline but not impossible value
+- stale reading that may still matter for internal diagnostics
+- duplicate event with inconsistent metadata
+- changed sensor calibration status
+- station relocation or coordinate drift
+
+### 4. Reviewed Internal Observation
+
+A reviewed internal observation may support local analysis, QA checks, or operator dashboards, but it still does not automatically belong on the public mirror.
+
+### 5. Approved Public Observation
+
+A public observation must:
+
+- have clear provenance
+- have valid or explicitly reviewed quality status
+- have acceptable freshness for its intended use
+- avoid private fields
+- avoid overstating scientific certainty
+- fit a reviewed public-safe export contract
+
+## Raw Vs Validated Vs Publishable
+
+| State | Meaning | Public use |
+| --- | --- | --- |
+| Raw | Received but not trusted yet | Never publish |
+| Validated | Deterministic checks passed | Internal only unless reviewed |
+| Publishable | Reviewed, bounded, provenance-clear, and contract-compliant | May enter a reviewed public export |
+
+## Freshness Boundaries
+
+Future sensor work must keep the same distinction already used in Clear Lake Watch:
+
+- resource freshness: whether the export or message bundle is recent enough to trust as an intake artifact
+- observation freshness: when the environmental reading was actually observed
+
+A recent file or recent MQTT delivery does not make an old reading current. A fresh observation in a delayed export is also not the same thing as live public monitoring. Both timestamps must remain visible to operators.
+
+## QA/QC Gates
+
+Minimum deterministic gates before internal use:
+
+- observation ID is unique
+- sensor ID is known
+- station ID is known
+- `observed_at` and `received_at` are parseable
+- `received_at` is not earlier than `observed_at`
+- parameter is from an approved list
+- unit matches the parameter contract
+- coordinates are present and plausible for the intended station
+- value is within parameter sanity limits
+- review status starts in a non-public state
+
+Recommended parameter sanity examples:
+
+- water temperature should not be below freezing for an unfrozen lake sample without explanation
+- dissolved oxygen should not be negative
+- pH should stay within physically plausible freshwater bounds
+- conductivity should use an expected unit family such as `uS/cm`
+
+## Quarantine Rules
+
+A record should be quarantined when any of these occur:
+
+- impossible value
+- stale reading beyond the allowed observation window
+- missing required timestamp
+- invalid unit
+- unknown sensor or station
+- coordinates outside the approved region
+- duplicate observation ID
+- broken JSON or missing required field
+
+Quarantined records stay out of reviewed exports and should move to a quarantine queue or dead-letter path with a machine-readable reason plus an operator-facing note.
+
+## Dead-Letter Handling
+
+Dead-letter handling is for records that cannot be trusted enough even for normal review flow.
+
+Use dead-letter handling when:
+
+- parsing fails
+- schema is broken
+- required identity fields are missing
+- the message cannot be associated with a known source
+
+Dead-letter storage should preserve:
+
+- raw payload or a safe copy
+- receipt timestamp
+- parser or validation error
+- gateway or intake identifier
+
+Dead-letter handling is an operations and audit surface, not a public data surface.
+
+## Human Review Requirements
+
+Human review should be required when:
+
+- values are suspect but not clearly invalid
+- a sensor recently changed calibration or firmware
+- publication wording could imply public-health authority
+- a record would become the newest public observation for a station or parameter
+- a station location or label changed
+- a bulk replay or outage-recovery import occurred
+
+Review questions:
+
+1. Does the record look physically plausible?
+2. Is the provenance clear enough to trust the source path?
+3. Is the freshness acceptable for the claimed use?
+4. Does the export remove private or sensitive fields?
+5. Would public display risk overstating certainty or recency?
+
+## Minimal Viable Deployment Path
+
+Keep the first deployment path small:
+
+1. one station naming convention
+2. one or two low-risk parameters such as water temperature or air temperature
+3. deterministic intake validator
+4. quarantine and dead-letter outputs
+5. reviewed JSON export for internal use
+6. only later, a reviewed public-safe export if the trust contract holds
+
+This packet does not authorize direct dashboard integration. The first credible milestone is a reviewed internal export path that proves fail-closed behavior.
+
+## Known Limitations
+
+- This packet does not implement MQTT, ingestion, storage, or review tools.
+- It does not prove any deployed sensor exists.
+- It does not define calibration SOPs in full detail.
+- It does not authorize public-health, recreation, or toxin-risk claims.
+- It does not replace mentor, lab, or agency review for sensitive variables.
+
+## Practical Boundary
+
+If future sensor work becomes convenient before it becomes reviewable, the project will mislead people. Clear Lake Watch should accept that some data stays private, stale, or quarantined rather than pretending raw telemetry is trustworthy enough for public interpretation.
diff --git a/docs/flagship-maturity-plan.md b/docs/flagship-maturity-plan.md
index 8c9673c..0607e4d 100644
--- a/docs/flagship-maturity-plan.md
+++ b/docs/flagship-maturity-plan.md
@@ -119,7 +119,7 @@ Deliverables:
- README keeps "current maturity" visible near the front door.
- Project brief stays concise and partner-friendly.
- Portfolio copy uses "prototype" and "situational awareness" language.
-- Dashboard validation passes before sharing. In static review mode, `-AllowStaleSnapshot` is acceptable; before broad publication, the snapshot should be refreshed or explicitly release-noted.
+- Dashboard validation passes before sharing. For a preserved static snapshot, its age must be release-noted; before broad publication, the snapshot should be refreshed or explicitly release-noted. (There is no `-AllowStaleSnapshot` override today — the validator warns on stale data but does not gate publication, so this stays a manual reviewer decision.)
- The portfolio-safe release scope focuses on presentation, validation evidence, screenshots, and conservative claims before live weather telemetry or public field submission.
### Phase 2: Trust Hardening
diff --git a/docs/mqtt_topic_conventions.md b/docs/mqtt_topic_conventions.md
new file mode 100644
index 0000000..bafbc87
--- /dev/null
+++ b/docs/mqtt_topic_conventions.md
@@ -0,0 +1,128 @@
+# MQTT Topic Conventions
+
+**Status:** naming proposal only
+**Audience:** future gateway builders, local operators, and reviewers
+
+This document proposes MQTT topic conventions for a future local-first sensor backbone. It does not implement a broker, client, gateway, or dashboard connection.
+
+The point of these conventions is to keep identity, routing, and quarantine behavior understandable before any live ingestion exists.
+
+## Proposed Topic Pattern
+
+```text
+clearlakewatch/sensors/{station_id}/{sensor_id}/observations
+clearlakewatch/sensors/{station_id}/{sensor_id}/status
+clearlakewatch/sensors/{station_id}/{sensor_id}/deadletter
+```
+
+## Naming Rules
+
+### Station Naming
+
+Use stable operator-readable station IDs.
+
+Examples:
+
+- `lakeport-demo-station`
+- `oaks-arm-north-01`
+- `lower-arm-shoreline-02`
+
+Recommended rules:
+
+- lowercase only
+- hyphen-separated
+- no spaces
+- no private device serials in the station ID
+
+### Sensor Naming
+
+Use sensor IDs that describe the instrument role without exposing secrets.
+
+Examples:
+
+- `temp-probe-01`
+- `do-sonde-01`
+- `ph-probe-01`
+
+Recommended rules:
+
+- stable across normal restarts
+- tied to inventory records
+- changed if hardware identity changes materially
+
+## Observation Messages
+
+Topic:
+
+```text
+clearlakewatch/sensors/{station_id}/{sensor_id}/observations
+```
+
+Use this topic for structured observation payloads that match the planned sensor data contract.
+
+Observation topics are still raw intake surfaces. Publishing to this topic does not imply:
+
+- the value is valid
+- the value is current enough for public interpretation
+- the value is approved for publication
+
+## Status Or Heartbeat Messages
+
+Topic:
+
+```text
+clearlakewatch/sensors/{station_id}/{sensor_id}/status
+```
+
+Use this topic for:
+
+- heartbeat signals
+- battery or uptime state
+- calibration-needed warnings
+- offline or degraded health messages
+
+Status messages help operators understand device state, but they should not become public lake-health signals.
+
+## Dead-Letter Messages
+
+Topic:
+
+```text
+clearlakewatch/sensors/{station_id}/{sensor_id}/deadletter
+```
+
+Use this topic for records that cannot safely enter normal validation or review flow.
+
+Examples:
+
+- broken payload
+- missing required identifiers
+- bad schema
+- parse failure
+
+Dead-letter topics are operational audit surfaces. They are not data products for the public mirror.
+
+## Why Raw Readings Must Not Go Directly To Public Dashboards
+
+Raw MQTT traffic is the wrong trust layer for public display because it can contain:
+
+- stale messages
+- duplicate observations
+- impossible values
+- unknown sensor IDs
+- private infrastructure details
+- temporary calibration failures
+
+The public dashboard should consume only reviewed exports, not broker topics. MQTT is an internal transport layer, not a publication contract.
+
+## Minimal Routing Model
+
+Recommended routing path:
+
+```text
+observations -> intake validator -> internal review queue or quarantine
+status -> operator diagnostics
+deadletter -> audit and repair workflow
+```
+
+This keeps the backbone fail-closed. If validation or review fails, the data should stop before it reaches any public layer.
diff --git a/docs/public-backlog.md b/docs/public-backlog.md
index 300a557..a183b76 100644
--- a/docs/public-backlog.md
+++ b/docs/public-backlog.md
@@ -84,7 +84,16 @@ These remain candidates for later work:
## Open Maintenance / Trust Issues
-No maintenance/trust issues are currently open in this public backlog snapshot.
+Freshness and publication-safety alignment follow-ups are now tracked here, opened after a claim-hygiene pass found that some safety-case wording described enforcement the validators do not yet perform:
+
+| Follow-up | Focus | Intent |
+| --- | --- | --- |
+| Implement a real stale-snapshot publication gate | Publication safety | Add an actual enforced gate (or reviewed override) so a stale snapshot cannot be published as fresh, replacing the current warn-only behavior and the previously documented but never-implemented `-AllowStaleSnapshot` control. |
+| Decide stale-source policy: fail vs warn | Publication safety | Make an explicit, documented decision on whether an over-threshold source observation should fail publication or remain a visible warning, and align the validators and docs to that decision. |
+| Unify Python and PowerShell validators | Reproducibility | Remove drift between `validate-public-mirror.py` (CI) and `validate-public-mirror.ps1` (local) by sharing one source of truth for required-text guards and freshness logic. |
+| Compute source and weather stale status | Data trust | Derive `status` / `machineReadableStatus` from observation age at build or render time instead of trusting static `"ok"` / `"partial"` fields that can misrepresent freshness. |
+
+These are documented as design/implementation follow-ups; none of them are enabled yet, and none change the current manual, reviewed publication posture.
## Open Reviewer-Readiness Issues
diff --git a/docs/public-mirror-boundary.md b/docs/public-mirror-boundary.md
index 2b50acc..016c454 100644
--- a/docs/public-mirror-boundary.md
+++ b/docs/public-mirror-boundary.md
@@ -74,7 +74,7 @@ Use `docs/publication-review-checklist.md` for the full publication decision gat
Before any broad public publish:
1. Refresh public data or intentionally document a static snapshot.
-2. Run `scripts/validate-dashboard.ps1` without `-AllowStaleSnapshot`.
+2. Run the public mirror validator and resolve any failures. (Staleness is reported as a warning, not a failure, and no `-AllowStaleSnapshot`-style flag exists — a stale snapshot is a manual reviewer decision.)
3. Confirm `.gitignore` still excludes private local records and SQLite stores.
4. Confirm the public app does not fetch `data/private/`, `*.local.json`, or detailed private review artifacts.
5. Capture a current screenshot if the publish is meant for portfolio promotion.
diff --git a/docs/public-snapshot-release-note-2026-05-13.md b/docs/public-snapshot-release-note-2026-05-13.md
index af89b57..6a9c6b9 100644
--- a/docs/public-snapshot-release-note-2026-05-13.md
+++ b/docs/public-snapshot-release-note-2026-05-13.md
@@ -17,7 +17,7 @@ This snapshot is not official public-health guidance, recreation guidance, emerg
## Static Snapshot Age Cue
-Reviewed on May 28, 2026: this committed public mirror is a static snapshot generated on May 5, 2026, so the dashboard files are 23 days old at this review point. The latest USGS observation in the snapshot is May 3, 2026, which is 25 days old at this review point.
+This committed public mirror is a static snapshot generated on May 5, 2026, with the latest USGS observation dated May 3, 2026. See the dashboard's snapshot-age badge for the current age of this reviewed static snapshot; the badge is computed at load time, so it does not go stale the way a hand-typed day count does.
Treat the page as a dated portfolio/review artifact until a new reviewed refresh is generated, validated, and published. The stale FHABS dates above are intentional warning cues, not current bloom observations.
diff --git a/docs/publication-review-checklist.md b/docs/publication-review-checklist.md
index f73d1ad..2654811 100644
--- a/docs/publication-review-checklist.md
+++ b/docs/publication-review-checklist.md
@@ -46,7 +46,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\refresh-live-data.
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\validate-dashboard.ps1 -SkipHttp
```
-Do not use `-AllowStaleSnapshot` for a fresh public publish.
+Do not publish a stale snapshot as if it were fresh. Note: the validator warns on a stale snapshot but does not block publication, and no `-AllowStaleSnapshot`-style override flag exists — freshness is a manual reviewer decision.
For an intentional static portfolio snapshot, write a release note or README note that names the snapshot date and why it is being preserved.
@@ -119,7 +119,7 @@ For the current local portfolio-safe release prep branch, see `docs/portfolio-re
Only publish after all of these are true:
-- validation passes without `-AllowStaleSnapshot`, or the release clearly explains a static snapshot
+- validation passes with no unresolved failures, and either the snapshot is fresh or the release clearly explains a preserved static snapshot (the validator only warns on staleness; it does not enforce this)
- private local files are excluded
- public claims match the maturity plan
- a current screenshot exists for promotion use
diff --git a/docs/reviewer-demo-notes.md b/docs/reviewer-demo-notes.md
index cf2071d..3c20481 100644
--- a/docs/reviewer-demo-notes.md
+++ b/docs/reviewer-demo-notes.md
@@ -7,7 +7,7 @@ These notes provide a short review path for Clear Lake Watch and captions for th
## Suggested Review Path
1. Open the public dashboard:
Source Status
- Source rows, observation dates, and generated output counts come - from the public snapshot manifest. + Source rows, observation dates, source-resource dates, and + generated output counts come from the public snapshot manifest.
+