{
  "available": true,
  "status": "adopted",
  "policy": {
    "quorumMinDomains": 3,
    "measurementWindowDays": 30,
    "reviewCadence": "quarterly",
    "notes": [
      "RTO is measured from fault detection to service restored, for a fault in the component itself. The worst case including dependency recovery is derived, not declared: see achievableRtoMinutes on the availability page.",
      "RPO applies to the data a service owns. A service whose durable state lives elsewhere records that in stateHeldIn and carries only a configuration RPO.",
      "Availability targets are stated over a rolling 30 days and are aspirations, not contracts: nothing here measures uptime yet. The honest figure is the incident log.",
      "This estate has two independent compute domains. Majority quorum needs three, so every stateful failover is manual and fenced by hand. That is a deliberate choice, not an oversight: automatic promotion across two sites turns a partition into split-brain."
    ]
  },
  "summary": {
    "classified": 20,
    "byClass": {
      "tier-0": 11,
      "tier-1": 6,
      "tier-2": 2,
      "tier-3": 1
    },
    "gaps": 3,
    "accepted": 6,
    "notes": 1,
    "singleNode": 12,
    "mustCluster": 11,
    "computeDomains": 3
  },
  "classes": [
    {
      "id": "tier-0",
      "name": "Fleet-fatal",
      "definition": "Loss stops the estate serving, or risks losing data that cannot be reconstructed. Everything else waits while this is being fixed.",
      "availabilityTarget": 99.5,
      "maxRtoMinutes": 30,
      "maxRpoMinutes": 15,
      "services": [
        "caddy",
        "google-oauth",
        "home1",
        "isp-uplink",
        "jarvisos-auth",
        "jarvisos-control-plane",
        "postgres-primary",
        "cloudflare-dns",
        "redis",
        "vin",
        "wireguard-tunnel"
      ]
    },
    {
      "id": "tier-1",
      "name": "Estate-critical",
      "definition": "The estate keeps serving without it, but a major capability or the ability to recover is gone. A tier-1 outage that lasts is how a tier-0 outage becomes unrecoverable.",
      "availabilityTarget": 99,
      "maxRtoMinutes": 240,
      "maxRpoMinutes": 1440,
      "services": [
        "gwscli-s3",
        "internal-dns",
        "nas",
        "nas-backups",
        "gwscli-s3-offsite",
        "postgres-replica"
      ]
    },
    {
      "id": "tier-2",
      "name": "Degraded-tolerable",
      "definition": "Loss is visible and annoying; nothing else is at risk. Fix it in hours, during waking ones.",
      "availabilityTarget": 95,
      "maxRtoMinutes": 480,
      "maxRpoMinutes": 10080,
      "services": [
        "jarvisos-worker",
        "vps1"
      ]
    },
    {
      "id": "tier-3",
      "name": "Best-effort",
      "definition": "Loss is accepted by policy. No objective, no page, no incident priority above P4.",
      "availabilityTarget": null,
      "maxRtoMinutes": null,
      "maxRpoMinutes": null,
      "services": [
        "nas-media"
      ]
    }
  ],
  "domains": [
    {
      "id": "fd-home-rack",
      "name": "Home rack",
      "kind": "site",
      "compute": true,
      "correlatedRisks": [
        "Mains power loss takes both hosts at once",
        "ISP uplink loss removes the whole domain from the network",
        "Fire, flood or theft — the only risk the offsite copy exists for",
        "Single-operator administrative error across both hosts"
      ],
      "services": [
        "home1",
        "isp-uplink",
        "jarvisos-control-plane",
        "postgres-primary",
        "redis",
        "internal-dns",
        "nas",
        "nas-backups",
        "nas-media"
      ]
    },
    {
      "id": "fd-vps",
      "name": "VPS provider",
      "kind": "site",
      "compute": true,
      "correlatedRisks": [
        "Provider or region outage",
        "Account suspension or billing failure — an administrative fault with the blast radius of a hardware one",
        "Monthly egress allowance exhaustion"
      ],
      "services": [
        "caddy",
        "jarvisos-auth",
        "vin",
        "postgres-replica",
        "jarvisos-worker"
      ]
    },
    {
      "id": "fd-vps-nbg",
      "name": "VPS provider · nbg1",
      "kind": "site",
      "compute": true,
      "correlatedRisks": [
        "Provider or region (nbg1) outage",
        "Account suspension or billing failure — shared with fd-vps, since both nodes sit behind one vendor account",
        "A co-tenant's root-level automation, which JarvisOS neither owns nor may stop",
        "Memory exhaustion caused by workloads this estate does not own"
      ],
      "services": [
        "vps1"
      ]
    },
    {
      "id": "fd-offsite",
      "name": "Offsite object storage",
      "kind": "site",
      "compute": false,
      "correlatedRisks": [
        "Vendor outage or account action",
        "Credential loss",
        "Loss of the encryption passphrase, which makes the copy unreadable — indistinguishable from not having it"
      ],
      "services": [
        "gwscli-s3",
        "gwscli-s3-offsite"
      ]
    },
    {
      "id": "fd-mesh",
      "name": "Private link plane",
      "kind": "vendor-plane",
      "compute": false,
      "correlatedRisks": [
        "A dynamic home-side IP change breaks the peer endpoint",
        "Loss of the link makes a healthy home rack indistinguishable from a dead one, seen from VIN",
        "Coordination-plane or relay dependence adds latency, not an outage"
      ],
      "services": [
        "wireguard-tunnel"
      ]
    },
    {
      "id": "fd-edge-dns",
      "name": "Public DNS plane",
      "kind": "vendor-plane",
      "compute": false,
      "correlatedRisks": [
        "DNS outage makes every published route unresolvable regardless of node health",
        "Certificate renewal depends on it — failures surface as TLS errors a day or two later"
      ],
      "services": [
        "cloudflare-dns"
      ]
    },
    {
      "id": "fd-identity",
      "name": "Identity plane",
      "kind": "vendor-plane",
      "compute": false,
      "correlatedRisks": [
        "New logins fail while existing sessions survive",
        "Every published route is forward-authed, so an identity outage reads as a total outage"
      ],
      "services": [
        "google-oauth"
      ]
    }
  ],
  "services": [
    {
      "id": "caddy",
      "name": "Caddy (reverse proxy)",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-vps",
      "stateClass": "rebuildable",
      "redundancy": "single-node",
      "mustCluster": true,
      "singleNode": true,
      "rtoMinutes": 10,
      "achievableRtoMinutes": 15,
      "achievableRtoDrivenBy": "vin",
      "rpoMinutes": null,
      "availabilityTarget": 99.5,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "TCP 443 probe every 120s",
        "verifies": "the TLS listener accepts connections",
        "intervalSec": 120,
        "unhealthyAfter": 1,
        "independent": true,
        "gap": "Accept-only. It does not prove a published route returns 200, and it cannot see a certificate that expires tomorrow."
      },
      "degradedMode": "Nothing published resolves to a working endpoint. Loopback and LAN access to home-rack services is unaffected, which is why the recovery console is deliberately loopback-only."
    },
    {
      "id": "google-oauth",
      "name": "Google OAuth",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-identity",
      "stateClass": "none",
      "redundancy": "vendor-ha",
      "mustCluster": true,
      "singleNode": false,
      "rtoMinutes": null,
      "achievableRtoMinutes": null,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": null,
      "failover": {
        "trigger": "vendor",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "reachability probe every 600s",
        "verifies": "the provider endpoint answers",
        "intervalSec": 600,
        "unhealthyAfter": 2,
        "independent": true,
        "gap": "Reachability, not a working authorisation round trip."
      },
      "degradedMode": "Existing sessions keep working; new logins fail. The estate keeps serving anyone already signed in, which is why session lifetime is an availability control and not just a security setting."
    },
    {
      "id": "home1",
      "name": "Home1",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "rebuildable",
      "redundancy": "active-passive",
      "mustCluster": true,
      "singleNode": false,
      "rtoMinutes": 30,
      "achievableRtoMinutes": 30,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": 99.5,
      "failover": {
        "trigger": "manual",
        "targetId": "vin",
        "promotionMinutes": 20
      },
      "healthCheck": {
        "signal": "jarvisos-agent heartbeat every 120s, observed from VIN",
        "verifies": "the host is alive and its agent can reach the console",
        "intervalSec": 120,
        "unhealthyAfter": 1,
        "independent": true,
        "gap": "Tunnel loss and host loss produce the same signal. Confirm from the LAN side before declaring a node outage."
      },
      "degradedMode": "Published routes on VIN keep serving. Anything hosted at home — control plane, Postgres primary, internal DNS — is down until the standbys are promoted."
    },
    {
      "id": "isp-uplink",
      "name": "ISP Uplink",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "none",
      "redundancy": "single-node",
      "mustCluster": true,
      "singleNode": true,
      "rtoMinutes": null,
      "achievableRtoMinutes": null,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": null,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "reachability probe every 300s",
        "verifies": "the home rack can reach the internet",
        "intervalSec": 300,
        "unhealthyAfter": 2,
        "independent": true,
        "gap": null
      },
      "degradedMode": "The home rack is unreachable from outside and the private link is down. VIN and everything on it keeps serving; use VIN as the vantage point for triage."
    },
    {
      "id": "jarvisos-auth",
      "name": "JarvisOS Auth (SSO)",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-vps",
      "stateClass": "rebuildable",
      "redundancy": "single-node",
      "mustCluster": true,
      "singleNode": true,
      "rtoMinutes": 15,
      "achievableRtoMinutes": 30,
      "achievableRtoDrivenBy": "redis",
      "rpoMinutes": null,
      "availabilityTarget": 99.5,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "self-reported status every 60s",
        "verifies": "the process is running",
        "intervalSec": 60,
        "unhealthyAfter": 1,
        "independent": false,
        "gap": "Nothing performs a real login. 'Existing sessions validate' and 'new logins work' are different claims, and only the second one proves the identity path end to end."
      },
      "degradedMode": "Forward-auth fails closed, so every published route returns an error even though the services behind it are healthy. An auth outage reads as a total outage. This console does not depend on auth precisely so that it stays usable during one."
    },
    {
      "id": "jarvisos-control-plane",
      "name": "JarvisOS Control Plane",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "rebuildable",
      "redundancy": "active-passive",
      "mustCluster": true,
      "singleNode": false,
      "rtoMinutes": 20,
      "achievableRtoMinutes": 30,
      "achievableRtoDrivenBy": "home1",
      "rpoMinutes": 60,
      "availabilityTarget": 99.5,
      "failover": {
        "trigger": "manual",
        "targetId": "vin",
        "promotionMinutes": 15
      },
      "healthCheck": {
        "signal": "self-reported status every 60s",
        "verifies": "the process is running and its own checks pass",
        "intervalSec": 60,
        "unhealthyAfter": 1,
        "independent": false,
        "gap": "A control plane that has stopped dispatching but still reports 'ok' is invisible here. The check that would catch it — dispatch a canary task and watch it complete — needs the node agent that does not exist yet."
      },
      "degradedMode": "No work is dispatched and no new workspaces are created. Running worker tasks continue to completion; their results are re-reported when the control plane returns."
    },
    {
      "id": "postgres-primary",
      "name": "Postgres (primary)",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "durable",
      "redundancy": "active-passive",
      "mustCluster": true,
      "singleNode": false,
      "rtoMinutes": 25,
      "achievableRtoMinutes": 25,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": 15,
      "availabilityTarget": 99.5,
      "failover": {
        "trigger": "manual",
        "targetId": "postgres-replica",
        "promotionMinutes": 10
      },
      "healthCheck": {
        "signal": "TCP 5432 probe every 60s, plus connection, disk and replication-lag metrics",
        "verifies": "the listener accepts connections",
        "intervalSec": 60,
        "unhealthyAfter": 1,
        "independent": true,
        "gap": "Liveness only. It does not prove the primary still accepts writes, and a full connection pool looks identical to an outage."
      },
      "degradedMode": "Reads can be served by the replica. Writes stop until a promotion decision is taken; the control plane refuses work rather than accepting it into a database that may be about to be fenced."
    },
    {
      "id": "cloudflare-dns",
      "name": "Public DNS",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-edge-dns",
      "stateClass": "none",
      "redundancy": "vendor-ha",
      "mustCluster": true,
      "singleNode": false,
      "rtoMinutes": null,
      "achievableRtoMinutes": null,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": null,
      "failover": {
        "trigger": "vendor",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "authoritative resolution probe every 900s",
        "verifies": "the expected records are returned by the authoritative nameserver",
        "intervalSec": 900,
        "unhealthyAfter": 2,
        "independent": true,
        "gap": null
      },
      "degradedMode": "Nothing published resolves. Clients with warm caches keep working until TTLs expire, which is why a correct fix can still look broken for minutes. LAN access by IP is unaffected."
    },
    {
      "id": "redis",
      "name": "Redis (cache / queue)",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "rebuildable",
      "redundancy": "single-node",
      "mustCluster": true,
      "singleNode": true,
      "rtoMinutes": 15,
      "achievableRtoMinutes": 30,
      "achievableRtoDrivenBy": "home1",
      "rpoMinutes": null,
      "availabilityTarget": 99.5,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "TCP probe every 60s",
        "verifies": "the listener accepts connections",
        "intervalSec": 60,
        "unhealthyAfter": 1,
        "independent": true,
        "gap": "Does not check that the queue is draining. A wedged consumer with a healthy Redis looks fine here."
      },
      "degradedMode": "Every session is invalidated: nobody is logged in and no published route authorises until Redis is back. Queued work re-runs."
    },
    {
      "id": "vin",
      "name": "VIN / VPS",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-vps",
      "stateClass": "rebuildable",
      "redundancy": "single-node",
      "mustCluster": true,
      "singleNode": true,
      "rtoMinutes": 15,
      "achievableRtoMinutes": 15,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": 99.5,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "jarvisos-agent heartbeat every 90s",
        "verifies": "the VPS is running and reachable",
        "intervalSec": 90,
        "unhealthyAfter": 1,
        "independent": false,
        "gap": "The console runs on VIN. It cannot observe VIN's own death — the last thing a dying node does is stop disagreeing with itself."
      },
      "degradedMode": "Everything published is offline. Home-rack services keep running on the LAN and are reachable from the home network, not from outside."
    },
    {
      "id": "wireguard-tunnel",
      "name": "WireGuard (Home1 <-> VIN)",
      "class": "tier-0",
      "status": "unknown",
      "domain": "fd-mesh",
      "stateClass": "none",
      "redundancy": "single-node",
      "mustCluster": true,
      "singleNode": true,
      "rtoMinutes": 15,
      "achievableRtoMinutes": 15,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": 99.5,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "handshake-age probe every 90s from both peers",
        "verifies": "the peers have completed a recent handshake",
        "intervalSec": 90,
        "unhealthyAfter": 1,
        "independent": true,
        "gap": "A recent handshake does not prove throughput. Replication can stall on a link that still handshakes."
      },
      "degradedMode": "The two domains are partitioned: replication stops, heartbeats stop, and a healthy home rack looks dead from VIN. Both sides keep serving their local clients. This is the failure most likely to be mistaken for a node outage."
    },
    {
      "id": "gwscli-s3",
      "name": "GWSCLI / S3",
      "class": "tier-1",
      "status": "unknown",
      "domain": "fd-offsite",
      "stateClass": "none",
      "redundancy": "vendor-ha",
      "mustCluster": false,
      "singleNode": false,
      "rtoMinutes": 240,
      "achievableRtoMinutes": 240,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": 99,
      "failover": {
        "trigger": "vendor",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "backup job reports success hourly; bucket quota and cost metrics",
        "verifies": "the bucket accepts writes and is inside quota",
        "intervalSec": 3600,
        "unhealthyAfter": 1,
        "independent": false,
        "gap": "A successful upload is not a verified restore. Only the quarterly drill proves this tier works."
      },
      "degradedMode": "Nothing serving is affected. The estate is running without a disaster-recovery copy until it returns, and the exposure grows by exactly the outage length."
    },
    {
      "id": "internal-dns",
      "name": "Internal DNS",
      "class": "tier-1",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "rebuildable",
      "redundancy": "active-passive",
      "mustCluster": false,
      "singleNode": false,
      "rtoMinutes": 20,
      "achievableRtoMinutes": 20,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": 99,
      "failover": {
        "trigger": "automatic",
        "targetId": "vin",
        "promotionMinutes": 1
      },
      "healthCheck": {
        "signal": "resolution probe every 300s",
        "verifies": "a known name resolves to the expected address",
        "intervalSec": 300,
        "unhealthyAfter": 2,
        "independent": true,
        "gap": null
      },
      "degradedMode": "The VIN secondary answers. Clients cache aggressively, so a fixed resolver can still look broken until TTLs expire — use IP addresses directly during recovery."
    },
    {
      "id": "nas",
      "name": "NAS",
      "class": "tier-1",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "rebuildable",
      "redundancy": "single-node",
      "mustCluster": false,
      "singleNode": true,
      "rtoMinutes": 120,
      "achievableRtoMinutes": 120,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": 99,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "jarvisos-agent heartbeat every 300s plus pool health and degraded-drive count",
        "verifies": "the host is alive and the storage pool still has redundancy",
        "intervalSec": 300,
        "unhealthyAfter": 1,
        "independent": true,
        "gap": "No SMART or scrub-age inspection yet: a pool can be healthy and still be one bad week from not being."
      },
      "degradedMode": "Serving is unaffected. Backups stop landing, which means the RPO of everything that backs up here starts aging silently — that is the real cost of a long NAS outage."
    },
    {
      "id": "nas-backups",
      "name": "NAS backup dataset",
      "class": "tier-1",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "durable",
      "redundancy": "single-node",
      "mustCluster": false,
      "singleNode": true,
      "rtoMinutes": 60,
      "achievableRtoMinutes": 120,
      "achievableRtoDrivenBy": "nas",
      "rpoMinutes": 60,
      "availabilityTarget": 99,
      "failover": {
        "trigger": "none",
        "targetId": "gwscli-s3-offsite",
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "backup job success timestamp, expected hourly, plus dataset usage",
        "verifies": "a backup landed within the RPO and the dataset has room",
        "intervalSec": 3600,
        "unhealthyAfter": 1,
        "independent": false,
        "gap": "A completed job is not a verified restore. The monthly restore-to-scratch drill is what proves this."
      },
      "degradedMode": "Serving is unaffected, but every RPO in the estate starts aging. An on-site backup outage is invisible until the moment it is not."
    },
    {
      "id": "gwscli-s3-offsite",
      "name": "Offsite backup replica",
      "class": "tier-1",
      "status": "unknown",
      "domain": "fd-offsite",
      "stateClass": "durable",
      "redundancy": "vendor-ha",
      "mustCluster": false,
      "singleNode": false,
      "rtoMinutes": 240,
      "achievableRtoMinutes": 240,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": 1440,
      "availabilityTarget": 99,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "sync job success timestamp, expected nightly",
        "verifies": "a sync completed within 24 hours",
        "intervalSec": 7200,
        "unhealthyAfter": 1,
        "independent": false,
        "gap": "Object listing is not checked automatically; a quarterly restore drill is the only real proof."
      },
      "degradedMode": "Nothing serving is affected. The estate is operating without disaster recovery, which is the failure mode nobody notices until the day it matters."
    },
    {
      "id": "postgres-replica",
      "name": "Postgres (replica)",
      "class": "tier-1",
      "status": "unknown",
      "domain": "fd-vps",
      "stateClass": "durable",
      "redundancy": "single-node",
      "mustCluster": false,
      "singleNode": true,
      "rtoMinutes": 45,
      "achievableRtoMinutes": 45,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": 0,
      "availabilityTarget": 99,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "TCP probe every 60s plus replication lag and disk metrics",
        "verifies": "the standby is up and streaming inside its lag threshold",
        "intervalSec": 60,
        "unhealthyAfter": 1,
        "independent": true,
        "gap": null
      },
      "degradedMode": "Nothing serving is affected while the primary is healthy. What is lost is the primary's failover target — tier-0 Postgres silently drops to backup-only recovery, which is a 25 minute restore instead of a 10 minute promotion."
    },
    {
      "id": "jarvisos-worker",
      "name": "JarvisOS Worker (VIN)",
      "class": "tier-2",
      "status": "unknown",
      "domain": "fd-vps",
      "stateClass": "ephemeral",
      "redundancy": "single-node",
      "mustCluster": false,
      "singleNode": true,
      "rtoMinutes": 30,
      "achievableRtoMinutes": 30,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": 95,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "self-reported status every 120s",
        "verifies": "the worker process is running",
        "intervalSec": 120,
        "unhealthyAfter": 1,
        "independent": false,
        "gap": null
      },
      "degradedMode": "Dispatched tasks queue instead of running. At-least-once delivery means they re-run when a worker returns."
    },
    {
      "id": "vps1",
      "name": "VPS1 / de-james-vps1",
      "class": "tier-2",
      "status": "unknown",
      "domain": "fd-vps-nbg",
      "stateClass": "none",
      "redundancy": "single-node",
      "mustCluster": false,
      "singleNode": true,
      "rtoMinutes": 60,
      "achievableRtoMinutes": 60,
      "achievableRtoDrivenBy": null,
      "rpoMinutes": null,
      "availabilityTarget": 95,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "jarvisos-agent heartbeat every 300s",
        "verifies": "the node is running and reachable, and can still be given work",
        "intervalSec": 300,
        "unhealthyAfter": 1,
        "independent": true,
        "gap": "Declared, not running. No JarvisOS agent is installed on this node — there is no /opt/jarvisos, no /etc/jarvisos and no jarvis unit on it — so this check has never returned anything and the node reads as unknown rather than healthy. Installing one is an owner decision about a machine another operator also administers, which is why it is a requirement and not a task. Until then the node's liveness is established by `jarvisos-independence.mjs probe`, on demand, by a person."
      },
      "degradedMode": null
    },
    {
      "id": "nas-media",
      "name": "NAS media dataset",
      "class": "tier-3",
      "status": "unknown",
      "domain": "fd-home-rack",
      "stateClass": "durable",
      "redundancy": "single-node",
      "mustCluster": false,
      "singleNode": true,
      "rtoMinutes": null,
      "achievableRtoMinutes": 120,
      "achievableRtoDrivenBy": "nas",
      "rpoMinutes": null,
      "availabilityTarget": null,
      "failover": {
        "trigger": "none",
        "targetId": null,
        "promotionMinutes": null
      },
      "healthCheck": {
        "signal": "agent-reported dataset usage every 3600s",
        "verifies": "the dataset is mounted and has room",
        "intervalSec": 3600,
        "unhealthyAfter": 1,
        "independent": false,
        "gap": null
      },
      "degradedMode": null
    }
  ],
  "findings": [
    {
      "code": "health-check-not-independent",
      "severity": "gap",
      "title": "Health is self-reported: a wedged process that still answers looks healthy.",
      "entityId": "vin",
      "detail": "VIN / VPS reports its own health (jarvisos-agent heartbeat every 90s); nothing outside it confirms.",
      "acceptance": null
    },
    {
      "code": "health-check-not-independent",
      "severity": "gap",
      "title": "Health is self-reported: a wedged process that still answers looks healthy.",
      "entityId": "jarvisos-control-plane",
      "detail": "JarvisOS Control Plane reports its own health (self-reported status every 60s); nothing outside it confirms.",
      "acceptance": null
    },
    {
      "code": "health-check-not-independent",
      "severity": "gap",
      "title": "Health is self-reported: a wedged process that still answers looks healthy.",
      "entityId": "jarvisos-auth",
      "detail": "JarvisOS Auth (SSO) reports its own health (self-reported status every 60s); nothing outside it confirms.",
      "acceptance": null
    },
    {
      "code": "single-node-unaccepted",
      "severity": "accepted",
      "title": "The class requires redundancy and there is none.",
      "entityId": "vin",
      "detail": "VIN / VPS is single-node but tier-0 requires a redundancy model.",
      "acceptance": {
        "reason": "VIN is the only public-ingress node in the estate. A second edge means a second VPS bill and DNS-level failover; the fleet's cost policy is zero new recurring spend. A 15 minute rebuild from the crown-jewels backup is the accepted answer.",
        "acceptedBy": "homelab-operator",
        "reviewOn": "2027-02-15"
      }
    },
    {
      "code": "single-node-unaccepted",
      "severity": "accepted",
      "title": "The class requires redundancy and there is none.",
      "entityId": "caddy",
      "detail": "Caddy (reverse proxy) is single-node but tier-0 requires a redundancy model.",
      "acceptance": {
        "reason": "Inherited from VIN being the only public-ingress node. A second proxy would need a second public node and DNS failover. The Caddyfile is managed configuration restored from the crown-jewels tier in minutes.",
        "acceptedBy": "homelab-operator",
        "reviewOn": "2027-02-15"
      }
    },
    {
      "code": "single-node-unaccepted",
      "severity": "accepted",
      "title": "The class requires redundancy and there is none.",
      "entityId": "redis",
      "detail": "Redis (cache / queue) is single-node but tier-0 requires a redundancy model.",
      "acceptance": {
        "reason": "Contents are rebuildable and a restart is seconds. A Sentinel cluster needs a third voting domain this estate does not have, and would introduce split-brain risk to buy availability for data we are willing to lose. The exposure is the auth coupling below, not the data.",
        "acceptedBy": "homelab-operator",
        "reviewOn": "2026-11-15"
      }
    },
    {
      "code": "single-node-unaccepted",
      "severity": "accepted",
      "title": "The class requires redundancy and there is none.",
      "entityId": "jarvisos-auth",
      "detail": "JarvisOS Auth (SSO) is single-node but tier-0 requires a redundancy model.",
      "acceptance": {
        "reason": "Inherited from VIN being the only public-ingress node; a standby would have nowhere independent to run. Restored in minutes from configuration, and the recovery console is deliberately reachable without it.",
        "acceptedBy": "homelab-operator",
        "reviewOn": "2027-02-15"
      }
    },
    {
      "code": "single-node-unaccepted",
      "severity": "accepted",
      "title": "The class requires redundancy and there is none.",
      "entityId": "wireguard-tunnel",
      "detail": "WireGuard (Home1 <-> VIN) is single-node but tier-0 requires a redundancy model.",
      "acceptance": {
        "reason": "One WAN path per side. A second path means a second circuit or an LTE failover device — recurring cost against a fleet policy of none. Mitigation is detection, not redundancy: the handshake probe exists so a partition is not misdiagnosed as a dead node.",
        "acceptedBy": "homelab-operator",
        "reviewOn": "2027-02-15"
      }
    },
    {
      "code": "single-node-unaccepted",
      "severity": "accepted",
      "title": "The class requires redundancy and there is none.",
      "entityId": "isp-uplink",
      "detail": "ISP Uplink is single-node but tier-0 requires a redundancy model.",
      "acceptance": {
        "reason": "One circuit, no second uplink. A backup uplink is recurring cost against a zero-new-spend policy, and the estate is designed so that the edge node keeps serving during a home outage.",
        "acceptedBy": "homelab-operator",
        "reviewOn": "2027-02-15"
      }
    },
    {
      "code": "stale-acceptance",
      "severity": "note",
      "title": "An accepted risk no longer matches any finding.",
      "entityId": "vps1",
      "detail": "An accepted risk for \"single-node-unaccepted\" no longer matches any finding; it can be removed.",
      "acceptance": null
    }
  ]
}