<!-- task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap plan=2 tag=REVIEW_REVIEW_BOOTSTRAP -->

# Plan - REVIEW_REVIEW_BOOTSTRAP

## 이 파일을 읽는 구현 에이전트에게

이 plan은 `code_review_cloud_G07_1.log`의 FAIL 판정에서 나온 repo-fixable 문제만 다룬다. 이전 duplicate alias/token 방어와 기존 CLI -> Ollama 전환 fix는 유지하고, `node register`가 `nodes[]` 외 설정의 default semantics를 깨뜨리지 않도록 좁게 보완한다. 구현 후 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 출력으로 채운 뒤 active 파일을 그대로 두고 리뷰를 요청한다.

## 배경

후속 구현은 invalid register mutation을 write 전에 거절했고 기존 CLI node의 Ollama 전환도 유효하게 만들었다. 하지만 `node register`가 raw YAML을 `config.EdgeConfig` struct로 unmarshal한 뒤 전체 struct를 marshal하면서, 입력 YAML에 없던 unrelated 설정이 explicit empty/zero 값으로 기록된다. 그 결과 기존에는 `config.LoadEdge` defaults로 동작하던 설정이 register 이후 빈 값으로 고정될 수 있다.

## 구현 체크리스트

- [ ] `node register` patches `nodes[]` and intentional `bootstrap.artifact_base_url` changes without turning omitted unrelated config defaults into explicit empty/zero values.
- [ ] Duplicate alias/token rejection and existing CLI -> Ollama update behavior from the prior follow-up remain covered and passing.
- [ ] Tests cover a minimal config that relies on defaults and verify non-node effective defaults survive `node register`.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 수정 대상

| 파일 | 항목 |
|---|---|
| `apps/edge/cmd/edge/main.go` | REVIEW_REVIEW_BOOTSTRAP-1 |
| `apps/edge/cmd/edge/main_test.go` | REVIEW_REVIEW_BOOTSTRAP-1, REVIEW_REVIEW_BOOTSTRAP-2 |

## [REVIEW_REVIEW_BOOTSTRAP-1] Preserve Non-Node Config Defaults

### 문제

`apps/edge/cmd/edge/main.go:503` unmarshals raw YAML directly into `config.EdgeConfig`, and `apps/edge/cmd/edge/main.go:650` marshals that entire struct back to disk. Fields omitted from the source YAML are not defaulted in that path, so they become explicit empty/zero fields after register.

Review repro:

```text
$ cat edge.yaml
server:
  listen: "0.0.0.0:9090"
openai:
  enabled: true

$ iop-edge --config edge.yaml config print
openai:
    enabled: true
    listen: 0.0.0.0:8080
    adapter: ollama
    session_id: openai
    timeout_sec: 120

$ iop-edge --config edge.yaml node register node-a --token token-a
$ iop-edge --config edge.yaml config print
openai:
    enabled: true
    listen: ""
    adapter: ""
    session_id: ""
    timeout_sec: 0
```

### 해결 방법

Use a YAML patch strategy instead of whole-struct rewrites, or otherwise load and write the effective config without zeroing omitted unrelated fields. Preferred shape:

- Parse the original file as `yaml.Node`.
- Decode only enough to get a working `config.EdgeConfig` for validation.
- Build the new/updated `config.NodeDefinition` in Go.
- Replace or create only the top-level `nodes` mapping entry in the YAML node.
- If `--artifact-base-url` is set, replace or create only `bootstrap.artifact_base_url`.
- Keep unrelated top-level sections and omitted fields untouched.
- Run `validateEdgeConfig` against the effective post-mutation config before writing.

Do not regress the prior behavior:

- invalid duplicate alias/token still fail before write/output;
- existing CLI -> Ollama still sets a valid fallback base URL or otherwise rejects before write;
- successful register still prints exactly the same one-line bootstrap command shape.

### 테스트 작성

Add a regression test such as `TestNodeRegisterPreservesDefaultedNonNodeConfig`:

- write a minimal config with `server.listen` and `openai.enabled: true`, intentionally omitting `openai.listen`, `openai.adapter`, `openai.session_id`, and `openai.timeout_sec`;
- capture `config print` or `config.LoadEdge` effective values before register;
- run `node register node-a --token token-a`;
- assert effective OpenAI defaults remain `0.0.0.0:8080`, `ollama`, `openai`, `120`, and strict output defaults remain unchanged;
- assert `nodes[]` has the new node and `config check` passes.

Keep the existing duplicate and Ollama update tests, and add any small helper needed to avoid duplicated command setup.

### 중간 검증

```bash
go test -count=1 ./apps/edge/cmd/edge -run 'TestNodeRegisterPreservesDefaultedNonNodeConfig|TestNodeRegisterRejects.*Duplicate|TestNodeRegister.*Ollama'
```

## [REVIEW_REVIEW_BOOTSTRAP-2] Full Register Regression

### 문제

The current target tests pass but do not protect the default-preservation contract. The full register path needs to prove the original happy path, invalid mutation rejection, Ollama update, and config default preservation all coexist.

### 해결 방법

Run formatting and the same package-level tests after the YAML patch change. If the patch implementation uses `yaml.Node`, keep helpers small and local to `apps/edge/cmd/edge/main.go`; avoid moving app internals into `packages/config`.

### 중간 검증

```bash
gofmt -w apps/edge/cmd/edge/main.go apps/edge/cmd/edge/main_test.go
go test -count=1 ./apps/edge/cmd/edge
```

## 최종 검증

```bash
go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/node ./packages/config
```

```bash
tmp="$(mktemp -d)" && go build -o "$tmp/iop-edge" ./apps/edge/cmd/edge && (cd "$tmp" && ./iop-edge config init --force && ./iop-edge node register node-silicon-ollama --adapter ollama --ollama-base-url http://127.0.0.1:11434 --target darwin-arm64 --artifact-base-url http://toki-labs.com:18080 && ./iop-edge config check)
```

Expected: tests pass without cache-only output; the temp command prints a final `curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s <token>` line and `config check` passes.
