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

# Inventory bounded query 계약 보완 계획

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

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

## 배경

첫 구현은 happy-path unit test와 env projection은 통과했지만 selector가 실제 inventory 값을 반환하지 않고 일부 provider shape를 누락했다. env/schema와 실행 exit 경계도 문서 계약과 일치하지 않았고, 제출된 검증 출력은 현재 module path와 불일치했다. bounded query가 canonical inventory 전체 읽기를 실질적으로 대체하도록 결과·오류·증거 계약을 함께 보완한다.

## 사용자 리뷰 요청 흐름

선택된 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_local_G06_0.log`
- Archived review: `agent-task/m-agent-readable-repository-refactor/13_inventory_query/code_review_local_G06_0.log`
- Verdict: FAIL
- Required: 4 — 실제 YAML value/전체 provider shape 누락, env/schema 검증 누락, `go run`과 0/1/2 exit 계약 불일치, 현재 module 및 representative selector와 맞지 않는 검증 evidence
- Suggested: 0
- Nit: 0 — review 중 미사용 `strings` import shim을 제거했고 targeted test가 통과했다.
- Affected files: `scripts/inventory-query/main.go`, `scripts/inventory-query/main_test.go`, `agent-test/README.md`, `agent-ops/skills/project/dev-runtime-deploy/SKILL.md`, `agent-ops/skills/project/dev-corp-runtime-deploy/SKILL.md`
- Verification evidence: isolated `GOCACHE`에서 targeted test, canonical env projection, `go test -count=1 ./...`, `git diff --check`는 PASS했다. 직접 binary는 zero-match=1/error=2였지만 `go run` shell status는 둘 다 1이었다. `--provider gx10-vllm`은 `nodes[1].provider`와 selector 문자열만 반환해 model provider mapping과 상세 값을 누락했다. 제출 로그의 `ip/...`는 현재/HEAD `module iop`와 불일치했다.
- Roadmap carryover: `inventory-query`는 PASS 전까지 미완료다.
- Allowed narrow reread: 위 archived plan/review 두 파일만 필요할 때 읽는다.

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/common/router.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-test/README.md`
- `agent-test/dev/inventory.yaml`
- `agent-test/dev-corp/inventory.yaml`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
- `scripts/inventory-query/main.go`
- `scripts/inventory-query/main_test.go`
- `agent-ops/skills/project/dev-runtime-deploy/SKILL.md`
- `agent-ops/skills/project/dev-corp-runtime-deploy/SKILL.md`
- `go.mod`
- `.gitignore`
- `agent-task/m-agent-readable-repository-refactor/13_inventory_query/plan_local_G06_0.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 원본 schema를 바꾸지 않고 read-only query의 내부 결과/오류 계약과 소비 문서만 바로잡는다.

### 테스트 환경 규칙

`test_env=local`이다. `agent-test/local/rules.md`와 `testing-smoke.md`를 적용하며 fresh targeted/full Go test, 실제 compiled CLI의 selector/exit 검증, `git diff --check`를 사용한다. `platform-common-smoke.md`도 확인했지만 packages/proto/config 원본을 바꾸지 않아 별도 platform-common 명령은 추가하지 않는다. 기본 Go cache의 stale permission 항목이 한 차례 재현되어 검증은 `/tmp`의 fresh `GOCACHE`를 사용한다. 원격 host, Docker, provider runtime은 이 read-only 도구 수정 범위가 아니며 필요하지 않다.

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

- node/provider/model match가 반환 path의 실제 YAML 값을 담는지 검증이 없다.
- dev의 provider mapping key와 dev-corp의 scalar provider list를 함께 검증하는 fixture/실 inventory assertion이 없다.
- unsupported env, env/file identity 불일치, required field/type 오류를 exit 2로 분류하는 검증이 없다.
- 현재 `TestQueryZeroMatch`는 helper 결과만 보고 CLI 0/1/2와 stdout/stderr를 검증하지 않는다.
- canonical 검증이 selector 없는 projection만 실행해 실제 node/model/provider shape 연결을 확인하지 않는다.

### 심볼 참조

외부 rename/remove는 없다. `match`, `queryNodes`, `queryProviders`, `queryModels`, `parseFlags`, `loadInventory`, `main`의 직접 참조는 `scripts/inventory-query/main.go`와 `main_test.go` 안에 한정된다. 소비 문서 참조는 `agent-test/README.md`와 두 deploy skill이다.

### 분할 판단

기존 split subtask `13_inventory_query`의 review follow-up이다. 결과 value, selector traversal, schema/exit, 소비 문서는 하나의 CLI 계약이며 따로 분리하면 다시 중간 불일치가 생긴다. 새 package/API, 다른 domain rollout, 외부 runtime 검증 경계가 없어 같은 디렉터리의 단일 follow-up이 안전하다.

### 범위 결정 근거

두 canonical `inventory.yaml`의 key/value/schema는 수정하지 않는다. deploy 실행 단계, 원격 host/provider 상태, API/proto/config/runtime 계약, roadmap 문서는 바꾸지 않는다. 새 third-party dependency, shell wrapper, repo-local generated binary를 추가하지 않고 임시 executable/cache는 `/tmp`에만 둔다.

### 빌드 등급

`cloud-G07`. 범위는 한 CLI로 제한되지만 shell/CLI workflow, stdout/stderr, 실제 process exit-status 계약과 이전 evidence 신뢰 회복이 중심이므로 cloud terminal-review 기준을 적용한다.

## 구현 체크리스트

- [ ] selector 결과가 반환 path의 실제 YAML 값을 담고 canonical node/model/provider shape를 결정적으로 모두 찾도록 구현한다.
- [ ] `dev|dev-corp` env allowlist와 최소 query schema 검증을 추가해 schema/file 오류를 exit 2로 분류한다.
- [ ] 테스트 가능한 CLI run 경계와 직접 실행 binary 기준 0/1/2 계약을 만들고 README/deploy skill 명령·fallback을 정렬한다.
- [ ] fixture regression, dev/dev-corp representative selector, fresh targeted/full Go test와 diff 검증을 실행해 실제 `iop/...` 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_IQRY-1] Selector value와 canonical traversal 보완

#### 문제

`scripts/inventory-query/main.go:39-41`은 모든 match value를 string으로 제한하고, `queryNodes`/`queryProviders`는 object path에도 selector 문자열을 넣는다. `scripts/inventory-query/main.go:237-324`의 provider 탐색은 hard-coded shape만 처리해 실제 dev mapping key와 dev-corp scalar provider list를 누락한다.

#### 해결 방법

반환 path와 `value`의 의미를 “그 path의 실제 YAML 값”으로 고정한다.

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

```go
type match struct {
	Path  string `json:"path"`
	Value string `json:"value"`
}
```

After:

```go
type match struct {
	Path  string `json:"path"`
	Value any    `json:"value"`
}
```

node는 `nodes[i]` object, model alias key는 alias config, provider mapping key/entity는 해당 map/list/scalar 값을 반환한다. provider collector는 이름이 `providers`인 map/sequence의 key/scalar/하위 `id`·`provider` exact match를 재귀 수집하고 기존 singular `provider`와 `direct_providers` shape도 보존한다. 모든 kind를 path 오름차순으로 정렬하고 path 기준 중복 제거한다.

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

- [ ] `scripts/inventory-query/main.go` — actual path value, generic provider collector, 공통 sort/dedupe 구현.
- [ ] `scripts/inventory-query/main_test.go` — canonical shape/value/sort regression 추가.

#### 테스트 작성

작성한다. `TestQueryNodeReturnsEntityValue`, `TestQueryModelAliasReturnsValueAtPath`, `TestQueryProviderAcrossCanonicalShapes`, `TestQueryNodeSortsByPath`에서 map key, scalar list, entity id/provider, 10개 이상 node path 정렬과 exact match를 assert한다.

#### 중간 검증

```bash
gofmt -d scripts/inventory-query/main.go scripts/inventory-query/main_test.go
GOCACHE=/tmp/iop-inventory-query-gocache go test -count=1 ./scripts/inventory-query -run 'TestQuery(NodeReturnsEntityValue|ModelAliasReturnsValueAtPath|ProviderAcrossCanonicalShapes|NodeSortsByPath)$'
```

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

### [REVIEW_IQRY-2] Env와 최소 schema 오류 분류

#### 문제

`scripts/inventory-query/main.go:145-151`은 env 존재 여부만 확인하고, `loadInventory`는 YAML parse 뒤 필수 field/type 또는 requested env 일치를 검증하지 않는다. 이 때문에 unsupported env는 파일 path로 흘러가고 schema 오류는 빈 projection 또는 zero-match가 된다.

#### 해결 방법

`parseFlags`에서 env를 `dev`, `dev-corp`로 제한한다. load 뒤 `validateInventory(data, requestedEnv, kind)`를 호출해 `test_env`, `profile`, `last_updated_at`, `source`, `edge`, `build`와 selector별 `model`/`nodes` 최소 shape를 검증하고, identity/type 오류를 명시 error로 반환한다. zero-match는 schema가 정상일 때만 1이다.

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

- [ ] `scripts/inventory-query/main.go` — env allowlist와 최소 schema validator 연결.
- [ ] `scripts/inventory-query/main_test.go` — unsupported env, identity/type/schema 오류 분류 테스트.

#### 테스트 작성

작성한다. `TestParseFlagsRejectsUnsupportedEnv`, `TestValidateInventoryRejectsIdentityMismatch`, `TestValidateInventoryRejectsInvalidProjectionTypes`, `TestRunClassifiesSchemaError`를 추가한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-inventory-query-gocache go test -count=1 ./scripts/inventory-query -run 'Test(ParseFlagsRejectsUnsupportedEnv|ValidateInventory.*|RunClassifiesSchemaError)$'
```

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

### [REVIEW_IQRY-3] Process exit와 소비 명령 정렬

#### 문제

`scripts/inventory-query/main.go:52-99`는 helper 내부에서 `os.Exit`하고, `agent-test/README.md:41`과 두 deploy skill은 `go run` shell status를 프로그램의 0/1/2로 설명한다. 실제 `go run`은 child error 2도 shell status 1로 감싸 fallback 분류 계약을 지킬 수 없다.

#### 해결 방법

CLI를 `run(args, stdout, stderr, loader) int` 경계로 분리하고 `main`만 `os.Exit(run(...))`을 호출한다. 문서와 deploy skill은 `/tmp`에 current checkout binary를 build한 뒤 직접 실행하는 표준 명령을 사용해 0/1/2를 보존한다. binary build 실패는 환경 blocker로, 직접 실행 exit 2만 schema/file fallback으로, exit 1은 zero-match로 구분한다.

Before (`agent-ops/skills/project/dev-runtime-deploy/SKILL.md:30-35`):

```markdown
- [ ] dev 환경은 `go run ./scripts/inventory-query --env dev`로 env projection을 먼저 확인한다.
- [ ] query가 schema/file 오류로 실패할 때만 canonical inventory 전체 읽기로 fallback한다.
```

After:

```markdown
- [ ] `go build -o /tmp/iop-inventory-query ./scripts/inventory-query`로 current checkout query binary를 만든다.
- [ ] `/tmp/iop-inventory-query --env dev ...`를 실행한다.
- [ ] 직접 실행 exit 2가 schema/file 오류일 때만 canonical inventory 전체 읽기로 fallback하고, exit 1은 zero-match로 보고한다.
```

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

- [ ] `scripts/inventory-query/main.go` — writer/loader 주입 가능한 `run`과 main 경계.
- [ ] `scripts/inventory-query/main_test.go` — 0/1/2와 stdout/stderr integration table test.
- [ ] `agent-test/README.md` — direct binary build/invocation과 exit 의미 명시.
- [ ] `agent-ops/skills/project/dev-runtime-deploy/SKILL.md` — dev query build/exit/fallback 정렬.
- [ ] `agent-ops/skills/project/dev-corp-runtime-deploy/SKILL.md` — dev-corp query build/exit/fallback 정렬.

#### 테스트 작성

작성한다. `TestRunExitCodesAndStreams`에서 success JSON은 stdout only/0, zero-match는 stderr only/1, flag/file/YAML/schema 오류는 stderr only/2임을 table로 assert한다.

#### 중간 검증

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

두 명령이 exit 0이어야 한다.

### [REVIEW_IQRY-4] Canonical selector와 evidence 신뢰 회복

#### 문제

이전 review의 canonical commands는 selector 없는 projection만 실행했고 출력 package path가 `module iop`와 다른 `ip/...`였다. 구현 bug를 실제 inventory shape에서 탐지하지 못했고 제출 evidence를 현재 checkout 결과로 신뢰할 수 없다.

#### 해결 방법

fixture test와 함께 dev node/provider, dev-corp model/provider representative selector를 직접 실행한다. Python JSON assertion은 exact path 존재, value type/content, projection 제외 key와 deterministic byte equality만 출력 없이 확인한다. fresh targeted/full test stdout을 그대로 review stub에 기록한다.

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

- [ ] `scripts/inventory-query/main_test.go` — 이전 Required를 재현하는 regression assertion 완성.
- [ ] `CODE_REVIEW-cloud-G07.md` — 실제 `iop/...` stdout/stderr와 compiled exit 결과 기록.

#### 테스트 작성

별도 제품 테스트 파일은 `main_test.go`에 추가한다. 실제 canonical inventory는 tracked data 연결이므로 아래 final command에서 직접 검증하고 fixture에 복제하지 않는다.

#### 중간 검증

```bash
/tmp/iop-inventory-query --env dev --provider gx10-vllm > /tmp/iop-inventory-dev-provider.json
/tmp/iop-inventory-query --env dev-corp --provider corp-dgx-spark-01-ornith > /tmp/iop-inventory-dev-corp-provider.json
python3 -m json.tool /tmp/iop-inventory-dev-provider.json >/dev/null
python3 -m json.tool /tmp/iop-inventory-dev-corp-provider.json >/dev/null
```

모든 명령이 exit 0이어야 한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `scripts/inventory-query/main.go` | REVIEW_IQRY-1, REVIEW_IQRY-2, REVIEW_IQRY-3 |
| `scripts/inventory-query/main_test.go` | REVIEW_IQRY-1, REVIEW_IQRY-2, REVIEW_IQRY-3, REVIEW_IQRY-4 |
| `agent-test/README.md` | REVIEW_IQRY-3 |
| `agent-ops/skills/project/dev-runtime-deploy/SKILL.md` | REVIEW_IQRY-3 |
| `agent-ops/skills/project/dev-corp-runtime-deploy/SKILL.md` | REVIEW_IQRY-3 |
| `CODE_REVIEW-cloud-G07.md` | REVIEW_IQRY-4 |

## 최종 검증

```bash
export GOCACHE=/tmp/iop-inventory-query-gocache
mkdir -p "$GOCACHE"
gofmt -d scripts/inventory-query/main.go scripts/inventory-query/main_test.go
go test -count=1 ./scripts/inventory-query
go build -o /tmp/iop-inventory-query ./scripts/inventory-query
/tmp/iop-inventory-query --env dev > /tmp/iop-inventory-dev-1.json
/tmp/iop-inventory-query --env dev > /tmp/iop-inventory-dev-2.json
cmp /tmp/iop-inventory-dev-1.json /tmp/iop-inventory-dev-2.json
/tmp/iop-inventory-query --env dev --node gx10-vllm > /tmp/iop-inventory-dev-node.json
/tmp/iop-inventory-query --env dev --provider gx10-vllm > /tmp/iop-inventory-dev-provider.json
/tmp/iop-inventory-query --env dev-corp --model gemma4:26b > /tmp/iop-inventory-dev-corp-model.json
/tmp/iop-inventory-query --env dev-corp --provider corp-dgx-spark-01-ornith > /tmp/iop-inventory-dev-corp-provider.json
python3 - <<'PY'
import json

def load(path):
    with open(path, encoding="utf-8") as handle:
        return json.load(handle)

env = load("/tmp/iop-inventory-dev-1.json")
assert "model" not in env and "nodes" not in env

node = load("/tmp/iop-inventory-dev-node.json")
node_match = next(item for item in node["matches"] if item["path"] == "nodes[1]")
assert node_match["value"]["id"] == "gx10-vllm-node"

provider = load("/tmp/iop-inventory-dev-provider.json")
provider_paths = {item["path"]: item["value"] for item in provider["matches"]}
assert isinstance(provider_paths["nodes[1].provider"], dict)
assert isinstance(provider_paths["model.active_edge_model_group.providers.gx10-vllm"], dict)

model = load("/tmp/iop-inventory-dev-corp-model.json")
model_paths = {item["path"]: item["value"] for item in model["matches"]}
assert isinstance(model_paths["model.aliases.gemma4:26b"], dict)

corp_provider = load("/tmp/iop-inventory-dev-corp-provider.json")
corp_paths = {item["path"]: item["value"] for item in corp_provider["matches"]}
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 --env dev --node definitely-not-a-node >/tmp/iop-inventory-zero.json 2>/tmp/iop-inventory-zero.err
zero_rc=$?
/tmp/iop-inventory-query --env unsupported >/tmp/iop-inventory-error.json 2>/tmp/iop-inventory-error.err
error_rc=$?
set -e
test "$zero_rc" -eq 1
test "$error_rc" -eq 2
test ! -s /tmp/iop-inventory-zero.json
test ! -s /tmp/iop-inventory-error.json
go test -count=1 ./...
git diff --check
```

모든 명령은 exit 0, gofmt/diff-check는 무출력이어야 한다. Go test는 fresh 실행하며 cache 결과를 허용하지 않는다. selector assertion은 secret/token 원문을 출력하지 않는다.

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