<!-- task=m-agent-readable-repository-refactor/13_inventory_query plan=2 tag=REVIEW_REVIEW_IQRY -->

# Inventory query 잔여 계약 보완 계획

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

구현과 검증을 마친 뒤 `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 명령 출력을 채우고 active 파일을 그대로 둔 채 리뷰 준비 상태를 보고한다. 종결, 로그 아카이브, `complete.log` 작성은 code-review 전용이다. 선택된 Milestone의 `구현 잠금 > 결정 필요` 항목이 실제 구현을 막는 경우에만 review stub의 `사용자 리뷰 요청`에 연결 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`/로그/`complete.log`를 만들지 않는다. 환경·secret·서비스 차단, 일반 범위 변경, 후속 에이전트가 채울 수 있는 evidence 공백은 사용자 리뷰 요청이 아니다.

## 배경

두 번째 구현은 실제 canonical selector, 직접 binary exit, fresh 전체 회귀를 통과했지만 계획이 약속한 provider map value traversal과 최소 schema type 검증을 모두 구현하지 않았다. `--help`도 `run` 경계를 우회해 parser가 프로세스를 직접 종료한다. 이미 정렬된 문서 계약은 유지하고 두 Go 파일의 잔여 동작과 regression만 보완한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone의 `구현 잠금 > 결정 필요`만 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active review stub에 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 요청 검증과 실제 `USER_REVIEW.md` 작성은 code-review가 담당한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
- Task ids:
  - `inventory-query`: env/model/node/provider selector로 canonical inventory의 bounded 결과를 얻는 결정적 query 경로
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- Prior task: `agent-task/m-agent-readable-repository-refactor/13_inventory_query`
- Archived plan: `agent-task/m-agent-readable-repository-refactor/13_inventory_query/plan_cloud_G07_1.log`
- Archived review: `agent-task/m-agent-readable-repository-refactor/13_inventory_query/code_review_cloud_G07_1.log`
- Verdict: FAIL
- Required: 3 — `providers` map scalar/entity value traversal 누락, projection/provider 최소 schema type 검증 누락, parser의 직접 `os.Exit`로 인한 `run` help 경계 위반
- Suggested: 0
- Nit: 0
- Affected files: `scripts/inventory-query/main.go`, `scripts/inventory-query/main_test.go`
- Verification evidence: fresh targeted/package/full Go test, gofmt, canonical dev/dev-corp selector, deterministic projection, compiled binary 0/1/2 exit, help process exit, `git diff --check`는 PASS했다. 현재 테스트가 위 세 잔여 계약을 다루지 않아 정적 대조에서 누락이 확인됐다.
- Roadmap carryover: `inventory-query`는 PASS 전까지 미완료다.
- Allowed narrow reread: 위 archived plan/review 두 파일만 필요할 때 읽는다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/rules/common/philosophy.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/testing-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
- `agent-spec/index.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `scripts/inventory-query/main.go`
- `scripts/inventory-query/main_test.go`
- `agent-task/m-agent-readable-repository-refactor/13_inventory_query/plan_cloud_G07_1.log`
- `agent-task/m-agent-readable-repository-refactor/13_inventory_query/code_review_cloud_G07_1.log`
- `agent-task/m-agent-readable-repository-refactor/13_inventory_query/code_review_local_G06_0.log`

### SDD 기준

Milestone은 `SDD: 불필요`다. 외부 API/proto/config schema나 runtime 책임 경계를 바꾸지 않는 동작 보존형 저장소 리팩터링이라는 기록 사유를 유지한다. 후속 구현은 inventory 원본과 소비 문서를 바꾸지 않고 query 내부의 이미 약속된 traversal/schema/help 계약만 완성한다.

### 테스트 환경 규칙

`test_env=local`이다. `agent-test/local/rules.md`와 `agent-test/local/testing-smoke.md`를 적용하며 fresh targeted/package/full Go test, 실제 compiled CLI의 canonical selector와 0/1/2/help 검증, `git diff --check`를 사용한다. `platform-common-smoke.md`도 확인했지만 packages/proto/config 원본을 바꾸지 않아 해당 profile 명령은 적용하지 않는다. 외부 host, Docker, provider runtime은 필요하지 않으며 직접 binary 실행이 이 read-only CLI의 full-cycle entrypoint 검증이다.

### 테스트 커버리지 공백

- `providers` map의 value가 selector와 같은 scalar인 경우와, map key는 다르지만 value object의 `id`/`provider`가 selector인 경우를 검증하지 않는다.
- `source`, `edge`, `build`의 잘못된 object type과 provider query의 `model`/`nodes` 양쪽 최소 shape를 검증하지 않는다.
- `run(--help)`가 주입 stdout을 사용하고 loader를 호출하지 않으며 0을 반환하는지 검증하지 않는다.

### 심볼 참조

외부 rename/remove는 없다. 수정 대상 `flagState`, `parseFlags`, `run`, `validateInventory`, `queryProviders`와 helper의 직접 참조는 `scripts/inventory-query/main.go`와 `scripts/inventory-query/main_test.go`에 한정된다.

### 분할 판단

split decision policy를 먼저 적용했다. 세 문제는 같은 `main.go`의 query/validation/CLI boundary와 같은 `main_test.go` regression에만 걸리고, 공유 API·다른 도메인·별도 검증 환경 경계가 없다. 하나를 따로 완료해도 `inventory-query` 계약은 닫히지 않으며 분할 시 같은 파일을 반복 수정하므로 기존 `13_inventory_query` 단일 follow-up이 안전하다.

### 범위 결정 근거

`agent-test/dev*/inventory.yaml`, `agent-test/README.md`, deploy skill, agent-spec, roadmap, API/proto/config/runtime 코드는 수정하지 않는다. query 문서와 canonical 대표 실행은 이미 현재 계약과 일치하므로 `scripts/inventory-query/main.go`, `main_test.go`, active review evidence만 갱신한다. 새 dependency와 repo-local binary/artifact는 추가하지 않는다.

### 빌드 등급

`local-G06`. 동작과 실패 재현이 두 Go 파일, deterministic unit test, 직접 binary command로 완전히 한정되고 외부/interactive 판단이 필요하지 않다.

## 구현 체크리스트

- [ ] `providers` collection의 map key, scalar value, entity `id`/`provider` match를 entry path의 실제 value로 결정적으로 수집한다.
- [ ] projection object type과 provider query의 `model`/`nodes` 최소 shape를 검증해 잘못된 schema를 exit 2로 분류한다.
- [ ] `--help`를 `run`의 주입 writer/return-code 경계로 옮겨 `main`만 `os.Exit`를 호출하도록 한다.
- [ ] fresh targeted/package/full Go test, canonical selector, compiled 0/1/2/help와 diff 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_IQRY-1] Provider map entry match 완성

#### 문제

`scripts/inventory-query/main.go:335-346`의 `providers` map branch는 selector와 같은 key만 결과로 추가한다. value가 selector인 scalar이거나 value object의 `id`/`provider`가 selector여도 `collect`는 generic `id`를 match하지 않고 scalar도 처리하지 않아 누락한다.

#### 해결 방법

`providers` collection의 각 entry를 공통 helper로 평가한다. map entry는 key exact match, scalar value exact match, object의 `id`/`provider` exact match 중 하나라도 충족하면 entry path와 entry value를 한 번만 추가한다. list entry도 같은 helper를 사용하고, nested provider collection 탐색은 유지한다.

Before (`scripts/inventory-query/main.go:335`):

```go
case map[string]interface{}:
    if targetVal, ok := p[selector]; ok {
        pPath := joinPath(childPath, selector)
        // key match only
    }
    for k, valItem := range p {
        collect(valItem, joinPath(childPath, k))
    }
```

After:

```go
case map[string]interface{}:
    for key, item := range p {
        entryPath := joinPath(childPath, key)
        addProviderEntryMatch(key == selector, item, entryPath, selector)
        collect(item, entryPath)
    }
```

helper는 scalar와 object `id`/`provider`를 평가하고 기존 path dedupe를 사용한다.

#### 수정 파일 및 체크리스트

- [ ] `scripts/inventory-query/main.go` — provider collection entry matcher와 map/list traversal 연결.
- [ ] `scripts/inventory-query/main_test.go` — map scalar, differently-keyed object `id`, differently-keyed object `provider`, stable path/value regression.

#### 테스트 작성

작성한다. `TestQueryProviderMatchesMapScalarAndNestedEntity`에 최소 세 map entry shape를 두고 각 결과가 collection entry path와 실제 scalar/object value를 반환하며 중복 없이 정렬되는지 assert한다.

#### 중간 검증

```bash
gofmt -d scripts/inventory-query/main.go scripts/inventory-query/main_test.go
GOCACHE=/tmp/iop-inventory-query-followup-gocache go test -count=1 ./scripts/inventory-query -run 'TestQueryProviderMatchesMapScalarAndNestedEntity$'
```

gofmt 출력이 없고 test가 exit 0이어야 한다.

### [REVIEW_REVIEW_IQRY-2] 최소 schema type 검증 완성

#### 문제

`scripts/inventory-query/main.go:211-218`은 `source`, `edge`, `build`의 존재만 확인해 scalar/list/null을 허용한다. `scripts/inventory-query/main.go:231-235`는 provider query에서 `nodes`만 확인하므로 `model`이 없거나 잘못된 타입이어도 partial success 또는 zero-match로 분류한다.

#### 해결 방법

env projection의 `source`, `edge`, `build`는 `map[string]interface{}`인지 검증한다. provider query는 전체 canonical provider shape를 순회하므로 `model` map과 `nodes` sequence를 모두 요구한다. model query의 `model.aliases`와 node query의 `nodes` 기존 검증은 유지한다.

Before (`scripts/inventory-query/main.go:211`):

```go
if _, ok := data["source"]; !ok {
    return fmt.Errorf("missing source")
}
```

After:

```go
if _, ok := data["source"].(map[string]interface{}); !ok {
    return fmt.Errorf("missing or invalid type for source")
}
```

provider kind에는 `model` map과 `nodes` sequence 검증을 함께 적용한다.

#### 수정 파일 및 체크리스트

- [ ] `scripts/inventory-query/main.go` — projection object type과 selector별 최소 schema 검증 보완.
- [ ] `scripts/inventory-query/main_test.go` — invalid projection object type, provider model/nodes 누락·오류, exit 2 regression.

#### 테스트 작성

작성한다. `TestValidateInventoryRejectsInvalidProjectionObjectTypes`, `TestValidateInventoryRequiresProviderModelAndNodes`, `TestRunClassifiesProviderSchemaError`를 table-driven case로 추가한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-inventory-query-followup-gocache go test -count=1 ./scripts/inventory-query -run 'Test(ValidateInventoryRejectsInvalidProjectionObjectTypes|ValidateInventoryRequiresProviderModelAndNodes|RunClassifiesProviderSchemaError)$'
```

모든 case가 exit 0이어야 한다.

### [REVIEW_REVIEW_IQRY-3] Help를 run 경계로 이동

#### 문제

`scripts/inventory-query/main.go:152-154`는 `parseFlags`가 실제 `os.Stdout`에 쓰고 프로세스를 직접 종료한다. 계획의 `main` 단독 `os.Exit` 계약을 깨고 `run`의 주입 writer, loader 미호출, help return code를 단위 테스트할 수 없다.

#### 해결 방법

`flagState`에 help 상태를 두거나 동등한 side-effect 없는 parse 결과를 반환한다. `parseFlags`는 `os.Exit`와 출력 I/O를 하지 않고, `run`이 help를 감지해 주입된 stdout으로 `printUsage`를 호출한 뒤 `exitOK`를 반환한다. `main`의 기존 `os.Exit(run(...))`만 프로세스 종료를 소유한다.

Before (`scripts/inventory-query/main.go:152`):

```go
case "--help", "-h":
    printUsage(os.Stdout)
    os.Exit(exitOK)
```

After:

```go
case "--help", "-h":
    fs.help = true
```

`run`은 loader 호출 전에 help를 처리한다.

#### 수정 파일 및 체크리스트

- [ ] `scripts/inventory-query/main.go` — side-effect 없는 help parse와 injected run output.
- [ ] `scripts/inventory-query/main_test.go` — help 0/stdout-only/loader-not-called regression.

#### 테스트 작성

작성한다. `TestRunHelpUsesInjectedStreams`에서 stdout usage, 빈 stderr, `exitOK`, loader 미호출을 assert하고 `-h`/`--help`를 table로 검증한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-inventory-query-followup-gocache go test -count=1 ./scripts/inventory-query -run 'TestRunHelpUsesInjectedStreams$'
GOCACHE=/tmp/iop-inventory-query-followup-gocache go build -o /tmp/iop-inventory-query-followup ./scripts/inventory-query
/tmp/iop-inventory-query-followup --help
```

test/build/help가 exit 0이고 help는 stdout에만 출력되어야 한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `scripts/inventory-query/main.go` | REVIEW_REVIEW_IQRY-1, REVIEW_REVIEW_IQRY-2, REVIEW_REVIEW_IQRY-3 |
| `scripts/inventory-query/main_test.go` | REVIEW_REVIEW_IQRY-1, REVIEW_REVIEW_IQRY-2, REVIEW_REVIEW_IQRY-3 |
| `CODE_REVIEW-local-G06.md` | 모든 항목의 구현/검증 evidence |

## 최종 검증

```bash
export GOCACHE=/tmp/iop-inventory-query-followup-gocache
mkdir -p "$GOCACHE"
gofmt -d scripts/inventory-query/main.go scripts/inventory-query/main_test.go
go test -count=1 ./scripts/inventory-query -run 'Test(QueryProviderMatchesMapScalarAndNestedEntity|ValidateInventoryRejectsInvalidProjectionObjectTypes|ValidateInventoryRequiresProviderModelAndNodes|RunClassifiesProviderSchemaError|RunHelpUsesInjectedStreams)$'
go test -count=1 ./scripts/inventory-query
go build -o /tmp/iop-inventory-query-followup ./scripts/inventory-query
/tmp/iop-inventory-query-followup --env dev --provider gx10-vllm > /tmp/iop-inventory-followup-dev-provider.json
/tmp/iop-inventory-query-followup --env dev-corp --provider corp-dgx-spark-01-ornith > /tmp/iop-inventory-followup-corp-provider.json
python3 - <<'PY'
import json

with open("/tmp/iop-inventory-followup-dev-provider.json", encoding="utf-8") as handle:
    dev = json.load(handle)
with open("/tmp/iop-inventory-followup-corp-provider.json", encoding="utf-8") as handle:
    corp = json.load(handle)

dev_paths = {item["path"]: item["value"] for item in dev["matches"]}
corp_paths = {item["path"]: item["value"] for item in corp["matches"]}
assert isinstance(dev_paths["model.active_edge_model_group.providers.gx10-vllm"], dict)
assert isinstance(dev_paths["nodes[1].provider"], dict)
assert corp_paths["model.aliases.ornith:35b.providers[0]"] == "corp-dgx-spark-01-ornith"
assert isinstance(corp_paths["nodes[0].provider"], dict)
PY
set +e
/tmp/iop-inventory-query-followup --help >/tmp/iop-inventory-followup-help.out 2>/tmp/iop-inventory-followup-help.err
help_rc=$?
/tmp/iop-inventory-query-followup --env dev --node definitely-not-a-node >/tmp/iop-inventory-followup-zero.out 2>/tmp/iop-inventory-followup-zero.err
zero_rc=$?
/tmp/iop-inventory-query-followup --env unsupported >/tmp/iop-inventory-followup-error.out 2>/tmp/iop-inventory-followup-error.err
error_rc=$?
set -e
test "$help_rc" -eq 0
test "$zero_rc" -eq 1
test "$error_rc" -eq 2
test -s /tmp/iop-inventory-followup-help.out
test ! -s /tmp/iop-inventory-followup-help.err
test ! -s /tmp/iop-inventory-followup-zero.out
test ! -s /tmp/iop-inventory-followup-error.out
go test -count=1 ./...
git diff --check
```

모든 명령은 exit 0, gofmt/diff-check는 무출력이어야 한다. Go test는 `-count=1` fresh 실행을 사용하고 binary/artifact는 `/tmp`에만 둔다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
