# Signal ownership registry for tools/watchdog_consistency_audit.py (a blocking
# infra-ci gate). Each signal is cross-validated against the live code-owned
# config that actually implements it:
#   primary_owner=internal  -> INFRA_PROBE_SPECS rendered from each service's
#                                deploy.py ProbeFacet declarations (#541;
#                                libs/probe_specs.py::render_probe_spec_text)
#   primary_owner=cloudflare -> cloudflare/infra-watchdog/wrangler.toml
#   primary_owner=github     -> tools/out_of_band_watchdog.py
#   primary_owner=excluded   -> intentionally unwatched; requires exclusion_reason
#                                + revalidation_condition
#   primary_owner=self       -> the check pages Feishu directly from its own code
#                                path (an in-process sidecar or a scheduled job's
#                                own alerting step), verified by that tool's own
#                                test suite rather than one of the three shared
#                                config sources above -- so it is exempt from
#                                this audit's owner-specific existence checks
#                                (see libs/container_breakdown_watch.py,
#                                tools/vault_self_refresh_audit_check.py).
#
# `tier` (minute|hour|day|month) + `type` (alert|report) -- #425 T5.
# ops.observability.md §2 is the SSOT law (landed by #425 T1/#426): ALERT is
# event-driven (fires only on a real failure), REPORT is time-driven (a
# periodic summary whose own delivery self-proves the channel); the
# cross-cutting invariant is tier<=hour => type=alert, tier>=day =>
# type=report (the alert/report boundary IS the day line).
#
# These two fields are OPTIONAL per signal -- backfilling every existing entry
# below is #425 T2's classify/audit job, tracked separately, not this gate's.
# But the moment a signal declares `type: alert`, tools/watchdog_consistency_
# audit.py._validate_tier_and_type() MAKES MANDATORY a structured (not
# free-text) debounce:
#   consecutive_failures: <int>   # consecutive bad polls before firing
#   renotify_window_sec:  <int>   # seconds between re-alerts on an active incident
# This is #425's own comment thread, verbatim: "an alert-tier check must
# declare not only {tier, type} but, for type=alert, a debounce/threshold =
# what distinguishes a real failure from a transient blip. That makes
# slow!=dead a structural rule, not per-check luck." It exists because this
# codebase shipped that exact bug TWICE independently before the rule existed
# -- libs/container_breakdown_watch.py (#475, 333 firing/resolved pairs in
# 48h from un-debounced flapping) and libs/vault_self_refresh_audit.py (#531,
# four separate instances of a check with no time-window semantics). The
# pre-existing free-text `alert_threshold: "3 consecutive failures"` field on
# many entries below is left as-is (not retrofitted into the structured
# fields here) -- converting the whole registry is more T2's classify-sweep
# than this gate's enforcement mechanism; new alert-type entries should
# prefer the structured fields, and may additionally carry `alert_threshold`
# as a human-readable summary.
#
# Hard cutover, not a ratchet: zero pre-existing entries declared `type:
# alert` before #425 T5, so making the debounce fields mandatory for
# `type: alert` breaks nothing already in this file -- see
# libs/tests/test_watchdog_consistency_audit.py for the positive/negative
# proof.
version: 2
owners:
  - internal
  - cloudflare
  - github
  - excluded
  - self
signals:
  # ── internal plane: GENERATED, do not hand-write entries here (#543) ──
  # Every `primary_owner: internal` entry is DERIVED from the ProbeFacet +
  # SignalFacet declarations on each service's deploy.py Deployer by
  # libs/watchdog_signal_entries.py::render_internal_signal_entries();
  # tools/watchdog_consistency_audit.py merges the derived entries into this
  # inventory at load time. Declaring a probe registers its signal — there is
  # no second list to keep in sync. The pre-#543 handwritten section is frozen
  # at libs/tests/fixtures/watchdog_internal_signals_frozen.yaml and
  # libs/tests/test_watchdog_signal_entries.py proves the derivation stays
  # field-level equivalent to it.
  #
  # ClickHouse liveness is deliberately NOT probed read-only: /ping stayed
  # green on an unwritable data dir (read green / write dead). It is covered
  # by a WRITE-PATH healthcheck (op-ch & platform-clickhouse compose ->
  # unhealthy -> Dokploy restart) plus the signoz/openpanel-roundtrip signals
  # (write+query) as the alert path.

  - signal_id: production.dokploy.public-route
    environment: production
    component: dokploy
    signal: dokploy-public-route
    primary_owner: cloudflare
    severity: critical
    cadence: 30m
    expected: "200,302"
    url: https://cloud.zitian.party

  - signal_id: production.vault.public-route
    environment: production
    component: vault
    signal: vault-public-route
    primary_owner: cloudflare
    severity: critical
    cadence: 30m
    expected: "200,429,472,473"
    url: https://vault.zitian.party/v1/sys/health

  - signal_id: production.minio.public-route
    environment: production
    component: minio
    signal: minio-public-route
    primary_owner: cloudflare
    severity: warning
    cadence: 30m
    expected: "200"
    url: https://minio.zitian.party/minio/health/live

  - signal_id: production.authentik.public-route
    environment: production
    component: authentik
    signal: authentik-public-route
    primary_owner: cloudflare
    severity: critical
    cadence: 30m
    expected: "200,204,302"
    url: https://sso.zitian.party/-/health/live/

  - signal_id: production.signoz.public-route
    environment: production
    component: signoz
    signal: signoz-public-route
    primary_owner: cloudflare
    severity: critical
    cadence: 30m
    expected: "200,302"
    url: https://signoz.zitian.party

  - signal_id: staging.minio.public-route
    environment: staging
    component: minio
    signal: minio-public-route
    primary_owner: cloudflare
    severity: warning
    cadence: 30m
    expected: "200"
    url: https://minio-staging.zitian.party/minio/health/live

  - signal_id: staging.authentik.public-route
    environment: staging
    component: authentik
    signal: authentik-public-route
    primary_owner: cloudflare
    severity: warning
    cadence: 30m
    expected: "200,204,302"
    url: https://sso-staging.zitian.party/-/health/live/

  - signal_id: staging.signoz.public-route
    environment: staging
    component: signoz
    signal: signoz-public-route
    primary_owner: excluded
    severity: warning
    url: https://signoz-staging.zitian.party
    exclusion_reason: SigNoz is a single global instance (prod_only); there is no staging deployment by design. All environments ship telemetry to the one collector and are separated by the deployment.environment attribute, so signoz-staging.zitian.party never exists and always 404s.
    revalidation_condition: Do not re-enable — signoz is global-by-design. Remove this signal only if SigNoz ever becomes a per-environment deployment.

  - signal_id: staging.dokploy.public-route
    environment: staging
    component: dokploy
    signal: dokploy-public-route
    primary_owner: excluded
    severity: warning
    url: https://cloud-staging.zitian.party
    exclusion_reason: cloud-staging currently returns HTTP 404.
    revalidation_condition: Enable after cloud-staging returns an intentional health status.

  - signal_id: staging.vault.public-route
    environment: staging
    component: vault
    signal: vault-public-route
    primary_owner: excluded
    severity: warning
    url: https://vault-staging.zitian.party/v1/sys/health
    exclusion_reason: vault-staging currently returns HTTP 404.
    revalidation_condition: Enable after vault-staging exposes an intentional health status.

  - signal_id: production.finance-report-web.public-route
    environment: production
    component: finance-report-web
    signal: finance-report-web-public-route
    primary_owner: cloudflare
    severity: critical
    cadence: 30m
    expected: "200,302,307,308"
    url: https://report.zitian.party/

  - signal_id: production.finance-report-api.public-route
    environment: production
    component: finance-report-api
    signal: finance-report-api-public-route
    primary_owner: cloudflare
    severity: critical
    cadence: 30m
    expected: "200"
    url: https://report.zitian.party/api/health

  - signal_id: staging.finance-report-web.public-route
    environment: staging
    component: finance-report-web
    signal: finance-report-web-public-route
    primary_owner: cloudflare
    severity: warning
    cadence: 30m
    expected: "200,302,307,308"
    url: https://report-staging.zitian.party/

  - signal_id: staging.finance-report-api.public-route
    environment: staging
    component: finance-report-api
    signal: finance-report-api-public-route
    primary_owner: cloudflare
    severity: warning
    cadence: 30m
    expected: "200"
    url: https://report-staging.zitian.party/api/health

  - signal_id: production.alerting-probes.heartbeat
    environment: production
    component: alerting
    signal: platform-alerting-probes
    primary_owner: cloudflare
    severity: critical
    cadence: 30m
    stale_threshold: 90m

  - signal_id: staging.alerting-probes.heartbeat
    environment: staging
    component: alerting
    signal: platform-alerting-probes-staging
    primary_owner: cloudflare
    severity: warning
    cadence: 30m
    stale_threshold: 90m

  - signal_id: global.infra2-public-entrypoint.github
    environment: global
    component: host
    signal: infra2-public-entrypoint
    primary_owner: github
    severity: critical
    cadence: 1d

  - signal_id: global.cloudflare-worker-health.github
    environment: global
    component: cloudflare-watchdog
    signal: cloudflare-worker-health
    primary_owner: github
    severity: critical
    cadence: 1d

  - signal_id: global.cloudflare-worker-status.github
    environment: global
    component: cloudflare-watchdog
    signal: cloudflare-worker-status
    primary_owner: github
    severity: critical
    cadence: 1d

  - signal_id: global.infra2-ssh.github
    environment: global
    component: host
    signal: infra2-ssh
    primary_owner: github
    severity: critical
    cadence: 1d

  - signal_id: global.infra2-docker.github
    environment: global
    component: docker
    signal: infra2-docker
    primary_owner: github
    severity: critical
    cadence: 1d

  - signal_id: global.infra2-docker-health.github
    environment: global
    component: docker
    signal: infra2-docker-health
    primary_owner: github
    severity: critical
    cadence: 1d

  - signal_id: global.infra2-alert-bridge.github
    environment: global
    component: alerting
    signal: infra2-alert-bridge
    primary_owner: github
    severity: critical
    cadence: 1d

  # Container-breakdown watch (#425 T5 registration; #475 is the debounce this
  # entry declares). Watcher PLUGIN inside the single resident alerting sidecar
  # (#543): libs/container_breakdown_watch.py running inside
  # tools/infra_probe_runner.py --loop (compose service `infra-probe-runner`,
  # container platform-alerting-probes) -- polling the Docker socket every
  # BREAKDOWN_INTERVAL_SECONDS=60s and paging Feishu directly (no separate
  # out-of-band watchdog verifies it, hence primary_owner=self; the merged
  # sidecar's state-file healthcheck + probe heartbeat cover its liveness).
  # tier=minute/type=alert: a crash-looping container hurts users within
  # minutes. The consecutive_failures/renotify_window_sec below are
  # libs.recency.evaluate_consecutive_hysteresis's fire thresholds -- exactly the
  # debounce whose ABSENCE caused #475 (333 firing/resolved pairs in 48h from a
  # single flapping container, each wrongly resetting the renotify clock).
  - signal_id: production.docker.container-breakdown-watch
    environment: production
    component: docker
    signal: container-breakdown-watch
    primary_owner: self
    severity: critical
    cadence: 60s
    tier: minute
    type: alert
    consecutive_failures: 3 # BREAKDOWN_FAILURE_THRESHOLD
    renotify_window_sec: 1800 # BREAKDOWN_RENOTIFY_SECONDS
    recovery_threshold: 5 # BREAKDOWN_RECOVERY_THRESHOLD (consecutive healthy polls to resolve; not a mandatory field, kept for completeness)
    alert_threshold: 3 consecutive broken polls (~3 minutes at 60s cadence)

  # Deploy-queue guard (#543 single-sidecar merge — this registration redeems
  # the #542 lint exemption that deferred it). Watcher PLUGIN inside the same
  # resident sidecar: libs/deploy_queue_guard.py inside
  # tools/infra_probe_runner.py --loop, sweeping Dokploy's deployment records
  # every DEPLOY_GUARD_INTERVAL_SECONDS=60s and paging Feishu directly
  # (primary_owner=self). environment=global: the guard watches the whole
  # shared Dokploy control plane (every project/env) from each runner.
  # consecutive_failures=1 is honest, not lax: a deploy only qualifies once it
  # has been `running` past DEPLOY_GUARD_CEILING_SECONDS (1800s) — the ceiling
  # IS the debounce (a 30-minute-sustained state cannot be a transient blip),
  # so the first qualifying sweep fires and renotify_window_sec suppresses
  # repeats (per compose_id) for the incident's lifetime.
  - signal_id: global.dokploy.deploy-queue-guard
    environment: global
    component: dokploy
    signal: deploy-queue-guard
    primary_owner: self
    severity: critical
    cadence: 60s
    tier: minute
    type: alert
    consecutive_failures: 1 # ceiling-qualified (DEPLOY_GUARD_CEILING_SECONDS=1800 is the real debounce)
    renotify_window_sec: 1800 # DEPLOY_GUARD_RENOTIFY_SECONDS
    alert_threshold: one sweep observing a deploy running past the 1800s ceiling

  # Vault self-refresh audit (#425 T5 registration; #531 is the recency-window
  # investigation this check exists to close). Daily GitHub Actions job --
  # .github/workflows/ops-checks.yml `vault-self-refresh-audit` (cron
  # "17 8 * * *") running tools/vault_self_refresh_audit_check.py, which SSHes
  # the VPS + reads live Dokploy compose env to inspect every service's
  # vault-agent/app-container self-refresh state. primary_owner=self: it pages
  # Feishu directly from its own workflow step (see ops-checks.yml's "Alert on
  # confirmed failure" step), not via a separate out-of-band watchdog.
  # tier=day/type=report, NOT alert: the job is silent on success and its only
  # notification is a page on a *confirmed* `status="fail"` result -- the same
  # silent-success/page-only-on-confirmed-failure shape this SSOT already
  # classifies deploy_v2_canary under (ops.observability.md §2 / SOP-007B), and
  # its once-daily cadence matches the harm-timescale of a slowly-staling Vault
  # secret (hours-to-days), not a minutes-scale liveness signal probe-runner
  # already owns. Because type=report, the alert-only consecutive_failures/
  # renotify_window_sec fields do not apply here: each scheduled run is a
  # fresh, independent one-shot audit with no consecutive-poll state to debounce
  # (see libs/vault_self_refresh_audit.py's module docstring for how
  # *within-a-run* staleness/recency is bounded instead, e.g.
  # RESTART_RECENCY_WINDOW_SECONDS).
  - signal_id: global.vault.self-refresh-audit
    environment: global
    component: vault
    signal: vault-self-refresh-audit
    primary_owner: self
    severity: critical
    cadence: 1d
    tier: day
    type: report

  # Daily facet reconcile (#542 task 4): ONE reality-vs-declaration run absorbing
  # the three formerly separate drift jobs (bespoke-app compose_id — previously
  # signal-less; config-hash drift; dns drift). ops-checks.yml `facet-reconcile`
  # (cron "17 7 * * *") running tools/facet_reconcile.py. primary_owner=self: it
  # delivers its own daily report (self-proving, #425) and pages Feishu directly
  # only on confirmed findings (#524 discipline). tier=day/type=report: silent-
  # success-report shape; drift harm timescales are hours-to-days.
  - signal_id: global.infra2.facet-reconcile
    environment: global
    component: iac
    signal: facet-reconcile
    primary_owner: self
    severity: critical
    cadence: 1d
    tier: day
    type: report
