<!-- task=m-node-provider-execution-liveness-recovery/04+03_health_evidence plan=1 tag=REVIEW_API milestone-task=health-classification -->

# PLAN — Fail-Closed Health Evidence and Test Readability

## For the Implementing Agent

Implement only this checklist, run every verification command, and fill the implementation-owned sections of `CODE_REVIEW-cloud-G05.md` with actual notes and output. Keep the active pair in place and report ready for review. If blocked, record the exact blocker, attempted command/output, and resume condition only in implementation-owned evidence fields. Do not ask the user, call user-input tools, create stop files, classify the next state, archive logs, or write `complete.log`; finalization belongs to the code-review skill.

## Background

The first official review found that terminal assembly can combine an inconclusive normalized health result with a stale definitive provider status, violating the approved three-pair S03 contract. The same review found two task-local readability violations in test files. This follow-up makes terminal health pairing fail closed and mechanically partitions the tests without changing watchdog ownership, timing, sequence, or wire scope.

## Archive Evidence Snapshot

- Closing pair: `agent-task/m-node-provider-execution-liveness-recovery/04+03_health_evidence/plan_cloud_G08_0.log` and `agent-task/m-node-provider-execution-liveness-recovery/04+03_health_evidence/code_review_cloud_G08_0.log`.
- Verdict: FAIL; Required findings are contradictory terminal health pairs, a 1,479-LOC `liveness_watchdog_test.go`, and an 830-LOC `provider_tunnel_test.go`. Suggested/Nit: none.
- Affected behavior/files: `liveness_health_evidence.go` terminal mapping and task-local watchdog/tunnel test organization.
- Verification evidence: fresh Node package tests, repeated Node/transport tests, vet, and `git diff --check` passed; a clean rerun of `IOP_DEV_RECONNECT_BIND_TIMEOUT=45 ./scripts/dev/edge-node-reconnect-diagnostic.sh` passed. `make readability-audit` named both task-local test files plus unrelated concurrent-worktree violations.
- Roadmap carryover: preserve `milestone-task=health-classification`; SDD S03 requires exactly `available/request_stalled`, `unavailable/provider_unhealthy`, or `unknown/health_unknown`, connection-scoped sequence evidence, and no progress reset.

## Analysis

### Files Read

- `agent-task/m-node-provider-execution-liveness-recovery/04+03_health_evidence/PLAN-cloud-G08.md`
- `agent-task/m-node-provider-execution-liveness-recovery/04+03_health_evidence/CODE_REVIEW-cloud-G08.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-provider-execution-liveness-recovery.md`
- `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`
- `agent-contract/inner/execution-runtime.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-spec/runtime/edge-node-execution.md`
- `apps/node/internal/node/liveness_health_evidence.go`
- `apps/node/internal/node/liveness_watchdog.go`
- `apps/node/internal/node/health_probe.go`
- `apps/node/internal/node/health_probe_test.go`
- `apps/node/internal/node/liveness_watchdog_test.go`
- `apps/node/internal/node/provider_tunnel_test.go`
- `apps/node/internal/node/run_handler.go`
- `apps/node/internal/node/tunnel_handler.go`
- `apps/node/internal/node/runtime_sink.go`
- `apps/node/internal/node/runtime_bridge.go`
- `apps/node/internal/transport/session.go`
- `apps/node/internal/transport/session_test.go`
- `packages/go/execution/liveness.go`
- `scripts/readability_audit.py`
- `scripts/readability_baseline.json`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`; status `[승인됨]`, lock released, no user review.
- Milestone scope: `milestone-task=health-classification`.
- Targeted scenario/evidence: S03 and its Evidence Map row require available/unavailable/unsupported/timeout fixtures, exact adapter/target/observation sequence evidence, and no original-request progress reset.
- This checklist derives the terminal pair regression from S03 and preserves all existing sequence, fence, normalized/tunnel, and no-reset evidence while repartitioning test files.

### Verification Context

- No neutral verification handoff was supplied. Repository-native sources are the Node/platform/testing domain rules, local Node and platform-common profiles, the original plan commands, `scripts/dev/edge-node-reconnect-diagnostic.sh`, and `scripts/readability_audit.py`.
- Preconditions: current checkout at `/config/workspace/iop-s1`; `go version` reported Go 1.26.2 linux/arm64 and `go env GOMOD` reported `/config/workspace/iop-s1/go.mod`; no credential, external provider, deployment, or remote host is required.
- Local full-cycle preflight: the diagnostic uses the current checkout, ephemeral local config/ports, mock provider, and repo-owned Edge/Node entrypoints. A clean 45-second rerun passed after module downloads completed.
- Readability constraint: concurrent unrelated work may keep the repository-wide ratchet nonzero. The deterministic JSON filter must show zero violations for every Go file in this follow-up, and every touched test file must be at or below 800 LOC.
- Confidence: high; the invalid pair is directly visible in terminal assembly and the audit JSON names both task-local test files.

### Test Coverage Gaps

- Existing three-way tests cover only internally consistent `HealthProbeEvidence` values; they do not cover `HealthUnknown` combined with a raw `available` or `unavailable` status.
- Existing normalized/tunnel sequence, metadata parity, fence, and no-reset tests are meaningful but concentrated in a file above the readability threshold.
- The successful tunnel health-scope assertion is meaningful but pushes its current file above the threshold.

### Symbol References

- No symbol is renamed or removed. `stallMetadata` is called only by `stalledRuntimeEvent` and `stalledTunnelFrame` in `apps/node/internal/node/liveness_watchdog.go`.
- Test functions move between same-package files; package-visible fixtures and production call sites remain unchanged.

### Split Judgment

- Keep one follow-up plan. Fail-closed pair construction and preservation of its normalized/tunnel regression suite form one compact contract repair; splitting the mechanical test moves would not create an independent behavioral PASS state.
- Dependency `03+02_health_probe_contract` is satisfied by `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/03+02_health_probe_contract/complete.log`.

### Scope Rationale

- In scope: terminal pair derivation, one contradictory-status regression, and mechanical partitioning of the two task-local oversized test files.
- Excluded: `health_probe.go` outcome semantics, watchdog timers/fences/cleanup, Session sequencing, Edge overlay/recovery, contracts/spec text, unrelated audit violations, and dispatcher/tooling changes. The current contracts/spec already state the intended behavior.

### Final Routing

- `evaluation_mode=isolated-reassessment`; finalizer `finalize-task-policy.sh`, pair mode.
- Build closures: scope/context/verification/evidence/ownership/decision all true; no capability gap. Scores `scope=1,state=0,blast=1,evidence=1,verification=2` -> G05. Base/final basis `local-fit`, lane `local`, filename `PLAN-local-G05.md`.
- Review closures: all true; no capability gap. Scores `scope=1,state=0,blast=1,evidence=1,verification=2` -> G05, official-review cloud, filename `CODE_REVIEW-cloud-G05.md`.
- `large_indivisible_context=false`; loop risks `boundary_contract`, `variant_product` (`count=2`); `review_rework_count=1`; `evidence_integrity_failure=false`; no risk/recovery boundary escalation.

## Implementation Checklist

- [ ] [FIX-1] Derive both terminal health fields from the normalized health result and add contradictory-status regression cases.
- [ ] [TEST-1] Partition watchdog and tunnel liveness tests into focused same-package files while preserving every fixture, assertion, and test name; keep each touched test file at or below 800 LOC.
- [ ] Run every command in Final Verification and record exact results in `CODE_REVIEW-cloud-G05.md`.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [FIX-1] Fail-closed terminal health pairing

**Problem:** `apps/node/internal/node/liveness_health_evidence.go:46-53` derives `provider_health` from the raw probe status but `liveness_classification` from normalized health. Identity mismatch, timeout recheck, or probe error with a definitive raw status can emit a pair outside the three S03 values.

**Solution:** Use the normalized health as the single authority for both terminal fields. Map `RequestStalled` to `available`, `ProviderUnhealthy` to `unavailable`, and every other value to `unknown`; retain `health_unknown` as the classification default.

Before (`apps/node/internal/node/liveness_health_evidence.go:46-53`):

```go
classification := obs.health.Health
if classification == "" {
	classification = runtime.HealthUnknown
}
metadata := map[string]string{
	"provider_health":         string(runtime.NormalizeProviderStatus(obs.health.Status)),
	"liveness_classification": string(classification),
}
```

After:

```go
classification := obs.health.Health
if classification == "" {
	classification = runtime.HealthUnknown
}
providerStatus := runtime.ProviderStatusUnknown
switch classification {
case runtime.RequestStalled:
	providerStatus = runtime.ProviderStatusAvailable
case runtime.ProviderUnhealthy:
	providerStatus = runtime.ProviderStatusUnavailable
}
metadata := map[string]string{
	"provider_health":         string(providerStatus),
	"liveness_classification": string(classification),
}
```

**Modified Files and Checklist:**

- [ ] `apps/node/internal/node/liveness_health_evidence.go` — enforce the exact pair mapping.
- [ ] `apps/node/internal/node/liveness_health_evidence_test.go` — add `TestStallMetadataFailsClosedOnContradictoryProbeStatus` covering raw available/unavailable with normalized `HealthUnknown` for normalized and tunnel terminal construction.

**Test Strategy:** Required bug regression. Assert both metadata maps and protobuf conversions emit `unknown/health_unknown`, never `available/health_unknown` or `unavailable/health_unknown`, while existing definitive pairs remain unchanged.

**Verification:** `go test -count=20 ./apps/node/internal/node -run '^(TestStallMetadataMapsThreeWayHealthEvidence|TestStallMetadataFailsClosedOnContradictoryProbeStatus)$'` exits zero.

### [TEST-1] Partition liveness evidence tests below the readability threshold

**Problem:** `apps/node/internal/node/liveness_watchdog_test.go:1` is 1,479 LOC and `apps/node/internal/node/provider_tunnel_test.go:778-830` raises that file to 830 LOC. Both are new task-local readability violations.

**Solution:** Preserve package boundaries and test names while moving cohesive blocks:

- Keep shared manual-clock fixtures and expiry/reset ordering tests in `liveness_watchdog_test.go`.
- Move `TestRunWatchdogLifecycle` through `TestTunnelConfirmedFenceClosesOwnershipBeforeTerminal`, including their private helpers, to `liveness_watchdog_lifecycle_test.go` with the complete imports `context`, `testing`, `time`, `google.golang.org/protobuf/proto`, `iop/packages/go/credentiallease`, `iop/packages/go/execution`, and `iop/proto/gen/iop`.
- Move `TestStalledTerminalsCloneSafeMetadata` through `TestWatchdogOmitsHealthObservationSeqWithoutBoundSession` to `liveness_health_evidence_test.go` with the complete imports `context`, `errors`, `testing`, `time`, `iop/packages/go/execution`, and `iop/proto/gen/iop`; add FIX-1 regression there.
- Move `TestNodeSuccessfulTunnelFramesCarryNoHealthEvidence` to `provider_tunnel_liveness_test.go` in package `node_test` with the complete imports `context`, `testing`, `time`, proto-socket, protobuf `proto`, `iop/packages/go/execution`, and `iop/proto/gen/iop`.
- Remove imports made unused by the moves and run `gofmt`; do not alter fixture behavior or assertions.

**Modified Files and Checklist:**

- [ ] `apps/node/internal/node/liveness_watchdog_test.go` — retain fixtures and ordering/race tests under 800 LOC.
- [ ] `apps/node/internal/node/liveness_watchdog_lifecycle_test.go` — receive lifecycle/fence/ownership tests.
- [ ] `apps/node/internal/node/liveness_health_evidence_test.go` — receive health metadata, probe join, sequence, and no-reset tests plus FIX-1 regression.
- [ ] `apps/node/internal/node/provider_tunnel_test.go` — remove only the health-scope success test.
- [ ] `apps/node/internal/node/provider_tunnel_liveness_test.go` — receive that same-package success test unchanged.

**Test Strategy:** Mechanical move plus existing deterministic coverage. Preserve every moved test name and assertion, then run fresh repeated/race suites. The LOC assertion and audit JSON filter are required evidence that the partition closes only task-local readability violations.

**Verification:** `go test -count=10 ./apps/node/internal/node ./apps/node/internal/transport`, the race suite, the deterministic LOC assertion, and the task-path audit filter all exit zero.

## Modified Files Summary

| Path | Action | Checklist |
|------|--------|-----------|
| `apps/node/internal/node/liveness_health_evidence.go` | modify | FIX-1 |
| `apps/node/internal/node/liveness_watchdog_test.go` | partition | TEST-1 |
| `apps/node/internal/node/liveness_watchdog_lifecycle_test.go` | create | TEST-1 |
| `apps/node/internal/node/liveness_health_evidence_test.go` | create | FIX-1, TEST-1 |
| `apps/node/internal/node/provider_tunnel_test.go` | partition | TEST-1 |
| `apps/node/internal/node/provider_tunnel_liveness_test.go` | create | TEST-1 |
| `agent-task/m-node-provider-execution-liveness-recovery/04+03_health_evidence/CODE_REVIEW-cloud-G05.md` | update evidence | all |

## Final Verification

1. `go version && go env GOMOD` — report the active Go toolchain and this module root.
2. `go test -count=20 ./apps/node/internal/node -run '^(TestStallMetadataMapsThreeWayHealthEvidence|TestStallMetadataFailsClosedOnContradictoryProbeStatus)$'` — PASS in all 20 iterations.
3. `go test -count=1 ./packages/go/execution ./apps/node/...` — PASS.
4. `go test -count=10 ./apps/node/internal/node ./apps/node/internal/transport` — PASS in all iterations.
5. `go test -race -count=3 ./packages/go/execution ./apps/node/internal/node ./apps/node/internal/transport` — PASS with no race report.
6. `go vet ./packages/go/execution ./apps/node/internal/node ./apps/node/internal/transport` — no diagnostics.
7. `go test -count=1 ./...` — repository Go suite PASS.
8. `./scripts/e2e-smoke.sh` — auxiliary smoke PASS.
9. `IOP_DEV_RECONNECT_BIND_TIMEOUT=45 ./scripts/dev/edge-node-reconnect-diagnostic.sh` — local registration, ordered payloads, commands, reconnect, and terminal checks PASS.
10. `python3 -c 'from pathlib import Path; paths=[Path(p) for p in ("apps/node/internal/node/liveness_watchdog_test.go","apps/node/internal/node/liveness_watchdog_lifecycle_test.go","apps/node/internal/node/liveness_health_evidence_test.go","apps/node/internal/node/provider_tunnel_test.go","apps/node/internal/node/provider_tunnel_liveness_test.go")]; bad={str(p):sum(1 for _ in p.open()) for p in paths if sum(1 for _ in p.open()) > 800}; assert not bad, bad'` — exits zero.
11. `make readability-audit` — run and record the full ratchet output. Exit zero is preferred; if unrelated concurrent-worktree violations remain, no entry may name a Go file in this plan.
12. `python3 -c 'import json; target={"apps/node/internal/node/liveness_health_evidence.go","apps/node/internal/node/liveness_watchdog_test.go","apps/node/internal/node/liveness_watchdog_lifecycle_test.go","apps/node/internal/node/liveness_health_evidence_test.go","apps/node/internal/node/provider_tunnel_test.go","apps/node/internal/node/provider_tunnel_liveness_test.go"}; data=json.load(open("build/readability-audit.json")); bad=[v for v in data["violations"] if v.get("path") in target]; assert not bad, bad'` — exits zero even when unrelated ratchet entries remain.
13. `test -z "$(gofmt -l apps/node/internal/node/liveness_health_evidence.go apps/node/internal/node/liveness_watchdog_test.go apps/node/internal/node/liveness_watchdog_lifecycle_test.go apps/node/internal/node/liveness_health_evidence_test.go apps/node/internal/node/provider_tunnel_test.go apps/node/internal/node/provider_tunnel_liveness_test.go)" && git diff --check` — exits zero.

**After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.**
