# 야외 구조물 참조 스텝(21.915)의 중복 구매 — 중앙 결과 조회 전용으로 (2026-09-03 새벽)

> GROUNDING-V2 goal 5 「뒤쪽 중복 유료 호출은 중앙 결과 조회 전용 또는 명시 adapter 로」의
> 마지막 남은 자리. TASKS 의 ⬜ 「21.915 를 read-only adapter 로」가 이것이다.
> 무료 조사(코드 · canary CP · 팩 원문)만으로 적었다. 유료 0.

## 1. 실측 — 무엇이 무엇에 닿는가

| 자리 | 사실 |
|---|---|
| `app/core/step_manifest.py:503` | 중앙 조사 `reference_acquisition`(19.3) 의 applicability 는 `if_grounding_reference` = `buys_v2_research(mode) or uses_chunk_producer(mode)` — **v2 와 v2_chunk 둘 다** 산다 |
| `app/modules/pipeline/reference_acquisition.py:246` | 야외 스텝의 소유권 문 `acquisition_owner` 는 `buys_v2_research(mode)` **만** 본다 = `mode == "v2"` |
| 따라서 | `v2_chunk` 에서 중앙 CP 가 **있어도** 소유자가 `outdoor` 로 나와 21.915 가 **같은 장소를 다시 산다** (검색 · 다운로드 · VLM 선택). 같은 규칙이 두 곳에 있어 한쪽만 고쳐진 부류 |
| `outdoor_structure_form_reference_step.py:1268` | 소유자가 앞쪽이어도 `_project_front_checkpoint` 가 `NotImplementedError` — `v2` 모드는 야외가 켜지면 여기서 **죽는다** |
| `app/core/config.py:1255,1261` | `outdoor_lane_plan_enabled` · `outdoor_lane_pipe_enabled` 기본 **True**. canary 는 `OUTDOOR_*=false` 로 껐기 때문에 두 결함 다 **안 보였다** |
| `prompts/_base/search_grounded_ref/11.202608311500/pick_system.md:18,24` | 야외 VLM 선택 지시가 「그 지역·시대에 실제로 지어진 대로」 · 「다른 나라·다른 시대면 뺀다」 — VLM 에게 **시대·나라를 판정시킨다**. goal 5 가 금지한 것. 중앙 조사는 거친 종류·가시성만 묻고 사람이 본다 |
| production DB | `project_settings.llm_config_json` 에 `grounding_mode` 를 선언한 프로젝트 **0** → 실제 production 은 전부 `legacy`(기본값). 위 두 결함은 v2/v2_chunk 를 켠 프로젝트에서만 난다 |

## 2. 그룹 → 장소 결속은 **ID 로** 있다 (이름 대조 불필요)

- 야외 대상은 lane plan 그룹(`gid`)이고, `background_classify.building_groups[gid].members[].loc_id`
  (`is_indoor == False`)가 `entity_merge.locations[].short_id`(`L01`)를 가리킨다
  (`outdoor_structure_seed.py:290-296`, `outdoor_place_spec.py:185`).
- 중앙 CP 의 장소 줄은 `ledger_row.final_id == "L01"` · `owner_type == "location"` · `purpose == "context"`,
  선택된 사진은 `acquisition.chosen_path`(projects 상대 경로). 부분은 `parent_final_id == "L01"`.
- 그러므로 `gid → loc_id → 중앙 줄` 은 **계보 변환**이고, 낱말 대조가 필요 없다.

## 3. 맞지 않는 한 가지 — 「필수」의 정의가 다르다

- 야외의 필수 그룹 = lane plan 이 `structure_plate` 로 결속한 것 (**합성에 사진이 필요한가**, 21.775 에서 결정).
- 중앙의 대상 = chunk 판독기의 hard/notice (**모델이 틀릴 만한가**, 7.5 에서 결정).
- 중앙 스텝(19.3)은 lane plan(21.775)보다 **먼저** 돌므로 야외 필수를 의무로 알 수 없다.
  중앙 소비자 중 가장 이른 것이 `episode_reference_policy`(21.65)라 중앙 스텝을 lane plan 뒤로
  옮기려면 21.65·21.70 도 같이 밀려야 한다 — **큰 재배치**, 이번 판이 아니다.

## 4. 제안 (순서대로 · 셋은 작고 넷째는 갈림)

1. **술어 하나** — `grounding_mode.buys_reference(mode)` 를 두고 manifest 의 `if_grounding_reference` 와
   `acquisition_owner` 가 **같은 함수**를 부른다. 두 곳에 있던 규칙을 한 곳으로.
2. **투영 구현** — `_project_front_checkpoint`: 대상 gid 마다 `loc_id` → `acquisition_projection(central_cp, required=True)` 의
   `location` 줄 → `outcome == selected` 면 `chosen_path` 를 열어 sha256 을 재서 기존 야외 CP 모양
   (`status ok · form_ref_path · form_ref_sha256 · candidate_count · source_website_url · fitting_refs [] · typology {} · audit{projected_from, final_id}`)
   으로 낸다. `mandatory_group_ids` · `target.no_spec_group_ids` · `policy_version` · `pack_version` 은 seed 계약(`resolve_form_reference_contract`) 그대로.
   네트워크 0 · 재검색 0 · 재선택 0 · bytes 복사 0.
3. **없으면 선다** — 필수 그룹의 장소에 중앙 `selected` 가 없으면(비대상 · unavailable · 사람 미선택) 422 로 서고
   사유에 `final_id` 와 중앙 상태를 적는다. 순수 T2I 로 조용히 내려가지 않는다(야외 계약 그대로).
   `v2_chunk` 는 production 기본이 아니라 지금은 아무 주행도 안 막는다.
4. **갈림(Codex 와 의논)** — 3 이 서는 경우를 어떻게 채우나:
   - (가) 야외 스텝이 **중앙 획득 함수**(`reference_acquisition_rounds.acquire_one` + 같은 brief 팩 · 같은 ChunkJournal 신원 · 같은 상한)를 명시 adapter 로 불러 그 장소의 `context` 의무를 늦게 산다. 구매는 한 번 · 장부는 하나. 대신 중앙 CP 밖에 늦은 줄이 생겨 `acquisition_projection` 이 두 CP 를 합쳐 읽어야 한다(합치는 자리 하나).
   - (나) 중앙 스텝을 lane plan 뒤로 옮긴다 — 소비자 21.65·21.70 재배치 필요. 깨끗하지만 크다.
   - (다) 3 그대로 두고 사람이 좌표 선언/의무 추가로 푼다 — 자동화 없음.
   내 추천은 **(가)**. 옛 야외 검색·VLM 선택(`_run_group`)은 legacy 모드에만 남고, 사는 모드에서는 호출 0.

## 5. 잠그는 시험 (무료)

- `acquisition_owner("v2_chunk", front_checkpoint_exists=True) == OWNER_FRONT` · manifest 술어와 **같은 함수**를 부르는지 AST 로.
- 투영: 중앙 CP fixture(장소 selected 1 · 비대상 1) + lane/building fixture → 야외 CP 모양 검증 · netprobe 0 · `_run_group` 호출 0.
- 없으면 선다: 필수 그룹의 장소가 비대상 → 422 · 사유에 final_id.

## 7. ★정정 (2026-09-03 02:30) — HITL 0 이 최상위 불변식

§6 의 「사람 확인·선택된 사진만」과 「사람 선택 뒤 재투영」은 **폐기**한다(사용자 재지시 · Codex 정정).
야외 투영은 날것 중앙 CP 의 `outcome == selected` 줄을 그대로 쓰고, `structure_form` 보충 구매도 자동으로
끝까지 간다(마지막엔 반드시 한 장). 사람 검토는 canary·A/B 도구의 사후 평가·override 일 뿐이다.
목적 일치(`structure_form` 의무만) · members cardinality 문 · fail-closed(순수 T2I 강등 금지)는 그대로.

## 6. Codex 판정 (2026-09-03 01:40) — ★§7 로 일부 정정됨 — 1·3 APPROVE · 2·(가) 는 문안 그대로면 BLOCK

확인한 근거(코드로 열어 봄):
- `acquisition_projection` 은 `final_id/status/outcome/fidelity` 만 내고 경로·bytes 를 안 낸다. 그리고 `outcome == selected` 는 **모델이 골랐다**는 뜻이지 사람이 확인했다는 뜻이 아니다.
  → 야외 투영은 검토가 반영된 입구 `grounding_fidelity_review.central_cp_with_reviews` + `reference_acquisition.usable_as_reference`(selected ∧ verified) 를 지나야 하고, 경로·SHA 는 `grounding_sidecar_writer.resolved_reference_path` / `row_content_sha256` 한 계약을 되쓴다(다시 적지 않는다).
- 중앙 `location#context` 와 야외 `structure_form` 은 **목적이 다르다** — 같은 final_id 라고 context 사진을 구조 형태 참조로 올리면 안 된다. 그 사진이 `structure_form` 목적으로도 사람이 확인·선택된 경우만 되쓴다.
- `building_groups[gid].members[]` 는 실외 `loc_id` 가 **여럿**일 수 있다 — 첫 항목/이름으로 고르지 말고 정본 대응이 정확히 하나인지 검증, 0개/여럿은 final_id 목록과 사유로 fail-closed.

좁힌 (가):
- 21.915 가 `outdoor_structure_form` 보충 의무를 **결정적으로** 만들고, low-level `acquire_one` 이 아니라 중앙 `ca.run` 의 전체 경계(두 pass · 재판정 · 좌표 · 예산 · 장부)를 부른다.
- 결과는 기존 `reference_acquisition` CP 를 고치지 않고 **별도 supplemental CP 에 append-only** 로 남긴다. `acquisition_projection` 이 두 CP 를 임의로 찾게 하지 않고, **야외 전용 공개 merge/view 한 곳**에서 base+supplement 를 obligation identity 로 합치고 중복·상충이면 선다.
- 새 후보는 unverified 이므로 사진 생성까지 자동 진행하지 않고 **사람 검토/선택**을 기다린다.
- 팩의 시대·나라 VLM 판정은 새 중앙 경로에서 쓰지 않고 legacy 에만 격리.

구현 순서: ① `buys_reference` SOT → ② ID/group cardinality 문 → ③ verified + purpose-matched read-only 투영 → ④ 없으면 supplemental `structure_form` acquisition → ⑤ 사람 선택 뒤 재투영. 야외 ON 이미지 생성은 이 acceptance 전 보류.

## 8. 구현 설계 — `structure_form` 보충 획득 (HITL 0 · 2026-09-03 03:05 초안 · Codex 검토 요청)

**언제**: 21.915 `_execute` 가 대상 gid 를 센 뒤(앞쪽이 소유자일 때). 사람 대기 없음.

**의무 만들기 (결정적)**: gid → `members[].loc_id`(실외 · 정확히 하나) → 중앙 CP 에 `obligation_kind == structure_form` 줄이
없는 장소마다 보충 의무 하나. 뼈대는 그 장소의 중앙 ledger_row 를 **복사**해 `research_subject_id = "<loc>#structure_form"` ·
`purpose None` · `obligation_kind structure_form` · `covers [loc]` · payload `coarse_type_label` = place spec 이 낸
`structure_desc`(런타임 LLM 산출 · 고정 명사 아님) · `search_terms_native` = 장소 검색어 · 원고 근거는 그대로. 5단계 조사가
「무엇인가 + 어찌 생겼는가」를 만든다.

**사기**: 중앙과 **같은 경계** `ca.run(supp_ledger, journal=<21.915 전용 장부>, cap=대상×(라운드×묶음+1), workdir=references/grounding/…,
search/download/judge/write_brief = 중앙 스텝과 같은 공장)`. 공장은 `reference_acquisition_step._judge/_write_brief` 를
모듈 함수(`grounding_acquisition_wiring.make_judge/make_writer`)로 빼서 두 스텝이 같은 것을 부른다(같은 규칙 두 곳 금지).

**남기기**: 별도 CP `reference_acquisition_supplement/manifest.json` — append-only(있던 줄은 유지 · 같은 subject 는 신원이 같으면
유지, 다르면 새 줄 추가 · `superseded_by` 표시). 중앙 CP 는 안 고친다.

**읽기**: 야외 전용 merge view `outdoor_reference_rows(front_cp, supp_cp)` 한 곳 — base + supplement 를 `research_subject_id` 로
합치고 base/supplement 가 같은 subject 를 둘 다 들면 선다. `_project_front_checkpoint` 는 이 view 만 읽는다.

**문**: 검색·받기·글 문은 프로세스 공용(canary 정지선 그대로). 보충 장부는 스텝 전용이라 중앙 상한과 안 섞인다.

**시험(무료)**: ①structure_form 줄이 없으면 보충 의무가 정확히 그 장소 수만큼 · ②있으면 0 · ③ca.run 이 같은 공장·같은 5단계로
불린다(AST) · ④supplement 는 append-only · ⑤merge view 충돌은 선다 · ⑥투영이 supplement 의 selected 를 form_ref_path 로 낸다 ·
⑦netprobe 0.


### §8 정정 (Codex 재리뷰 2026-09-03 03:40 · 56047391)
- 의무를 막는 것은 **중앙(base) CP 의 `structure_form` 줄뿐**. 기존 보충 CP 는 의무를 안 막는다 — 옛 줄이 retryable/unavailable 이거나
  desc·좌표·팩이 바뀌어 새 신원이 필요해도 `ca.run` 에 닿고, 되쓰기·재시도·신원은 `ca.run`/장부가 판단한다.
- merge 는 같은 신원 **그리고** 같은 유효 결과(status·outcome·chosen_path·match_quality·why_unbought)일 때만 no-op. 회복/변경은
  옛 줄 `superseded_by` + 새 줄 append. 살아 있는 줄이 subject 당 둘 이상이면 선다. 파일은 `checkpoint_io.atomic_write_json`.
- 구조 오류(장소 0/여럿 · 서술 결손 · 중앙 장소 부재)는 `validate_targets` 가 **provider 앞에서 한 번에** 세운다(422). `ca.run` 에 `stop_check` 전달.
- 문구: 사람 판정은 어디에도 조건이 아니다 — 붙는 조건은 자동 선택(`outcome == selected`) 하나.

### §8 정정 둘 (실측 canary `4398a55dc0bb` 2026-09-03 04:26~04:43 · Codex 재리뷰 04:52)

**무엇이 났나.** 야외 ON 최소 닫힘(target `outdoor_structure_form_reference`)을 유료로 돌렸더니 exit 0 인데 보충 CP 0 ·
journal 0 · `projected_from None`. 실제 수: building_groups 3 · 실외 그룹 **1**(bg_alley) · unique 실외 location **1**(L02).
`step_manifest` 에 `outdoor_structure_form_reference → reference_acquisition` 의존이 **없어** 닫힘 32 스텝에 중앙이 빠졌고
(step_run 에도 없음), 앞쪽 CP 가 없으니 `acquisition_owner(v2_chunk, front=False)` 가 「명시적 legacy fallback」으로 내려가
야외 스텝이 제 5라운드 구매를 했다(llm_call_log 4회). acceptance ①②⑥⑦ ✅ / ③④⑤ ❌ → **미확정**. 자동 재실행 안 함.

**고친 것 (side worktree → main 병합 대기).**
1. `918e1fbb` — manifest `depends_on` 에 `reference_acquisition`(order 19.3 < 21.915). `acquisition_owner` 는 `buys_reference(mode)`
   인데 front 가 없으면 `FrontCheckpointMissing` — 사지 않고 선다 → 스텝 422 `form_reference.front_missing`. legacy 소유자는
   **사지 않는 모드에서만** 남는다(시험 대역·부분 조립). 옛 fallback 을 잠근 시험은 뒤집었다.
2. `93dd009a` — (Codex BLOCK) 소유권 전환이 step-local config hash 에 안 실려 옛 legacy 구매형 completed CP 가 v2/v2_chunk 재개에서
   그대로 되쓰일 자리였다. `buys_reference(mode)` 일 때만 payload 에 `central_ownership`(mode · OWNER_FRONT ·
   `ACQUISITION_CONTRACT_VERSION` · `SUPPLEMENT_CONTRACT_VERSION` · 새 `FRONT_PROJECTION_CONTRACT_VERSION`[소비 경계]) 를 싣는다.
   legacy hash 는 byte-identical(시험) · 계약 하나만 바꿔도 움직임(시험) · 옛 legacy CP 를 사는 모드 runner 가 어긋남으로 봄(끝점 시험).
3. `e3d6a104` — 그룹 상한 문: `assert_outdoor_group_cap_covers` 가 `background_classify` CP 를 production 술어 한 곳
   (`outdoor_place_spec_step.outdoor_groups_of`)으로 세고, `run_pipeline(before_step=)` 이 그 단위 스텝을 부르기 직전(provider 앞)에
   부른다. 네 producer 상한(outdoor_group · outlook_pair · state_variant · entity)을 `PRODUCER_CAP_GATES` 한 표로.

**분모 고정 (Codex).** 그룹 수 = 계측·상한 분모. 보충 행 = **필수 unique location 마다 live 1행**(`structure_form_obligations` 가 같은
loc 을 seen 으로 접는다).

**다음 판 순서 (Codex).** 병합 → **기존 run `4398a55dc0bb` 에 transition+dry 먼저**(시나리오 축은 같고 코드/닫힘만 바뀜 — 상류 CP 를
되쓰고 새로 든 `reference_acquisition` 만 돌리며 야외 스텝은 새 hash 로 force) → 명시적 scope 계약으로 거절될 때만 그 사유를 적고
fresh run(`f7cc45c576c0`) → live acceptance 는 groups=1 · unique loc=1 을 갈라 적고 supplement live row=1 · projected path/SHA/identity ·
같은 run 재개 provider 0 까지.


### §8 정정 셋 (같은 run 재개 2판 · 2026-09-03 05:04~05:17)

**결과.** 야외 보충 acceptance **①~⑦ 전부 통과**(`outdoor_acceptance.json`): supplement live 1행 `L02#structure_form` · projection `bg_alley` ok ·
`projected_from reference_acquisition` · path/SHA/identity · HITL 0 · 같은 run 재개 provider 0(llm_call_log 44→44). 실제 수: 실외 그룹 1 · unique loc 1.

**그 사이 잡은 것 셋.**
1. (Codex 긴급 BLOCK 05:08) 중앙 상한이 **계획 때** 수였다 — dry 때 `grounding_screen` CP 가 없어 선언 하한 5 로 25 를 열었고 같은 live 안에서 screen 이
   의무 20 을 세웠다. → `assert_central_cap_covers` 가 `reference_acquisition` 직전(provider 앞)에 screen CP 의 `counts.obligation` 과 `plan_basis.central_target_count`
   를 견줘 넘으면 사지 않고 선다(`faf70a87`). 계획 근거는 원고 치수(dims)와 섞지 않는다 — 섞자 `assert_scope` 가 같은 run 재개를 「치수가 다르다」로 세웠다(`00f02dc1`).
2. force 될 끝난 스텝(지문 어긋남)이 dry 의 남은 표·상한 문·단위 확인에서 done 으로 쳐져 계획에서 빠졌다 → `forced_for_drift_of`(`5dd3b92e`).
3. 정지 요청(`step.cancelled`)이 라운드 runner 의 `except Exception` 에 「검색 실패」로 접혀 두 라운드가 실패로 적히고 9행이 why "" · disposition acquired 로 남았다
   → `run_control.is_abort` 한 곳의 규칙로 세 except(저작·검색·판정)에서 올리고, 행 위 `why` 는 마지막 라운드 까닭(`row_why`)(`cd210f76`).

**남은 물음(Codex 검토 중).** retryable 9 인데 중앙 스텝이 completed 로 봉인된 것 — 다시 열지(`--steps reference_acquisition`, ≈30콜) 또는 완료 판정을 고칠지.

### §8 정정 넷 (Codex 결정 2026-09-03 05:30 · 취소 = 자동 재시도 빚)

**결정.** 야외 보충 배선은 7/7 APPROVE. BLOCK 은 중앙 쪽: 취소에 맞은 9줄이 raw `retryable` · 후보 0 인데 `rows_to_rejudge` 가 후보 있는 줄만
세어 `failed_count 0 · completed` 로 봉인됐고, plain resume 은 verify pass → SKIP 으로 지나갈 자리였다. 「취소를 미완료로 남겨 일반 재개가
처리한다」는 그 코드로는 성립하지 않았다.

**계약 (①~⑤, main `fe428dae` · `3ebd32dc` · `85617c9c`).**
1. `ReferenceAcquisitionStep.pending_rows`(공개) — 후보 있는 retryable = **재판정 빚**(rejudge), 후보 없는 retryable = **재조사 빚**(research_retry),
   둘 다 `total` 로 미완료. terminal(`selected` · `no_match_after_retry`)·이미 고른 줄·안 산 줄은 안 센다.
2. `central_wrap.failed_count` 와 `verify_completion` 이 같은 `total` 을 쓴다. `retryable` 은 `reference_unavailable` 로 보여도 raw terminal 이
   아니라 completed 봉인 금지. `no_match_after_retry` 만 terminal unavailable — production 은 두 라운드 정상 완주·후보 0 을 그 값으로 적는다(rounds:595).
3. 사람 대기 문구 없음 — 자동 재시도 빚이다(HITL 0).
4. 끝점: 실물 모양 CP(selected 7 · retryable 9 후보 0 · not_applicable 10)를 production `StepRunner._evaluate_resume_decision(resume)` 에 넣으면
   RERUN_SELF(origin `artifact_missing` — cleanup 없음). 깨끗한 CP 는 SKIP. 장부: `ok` 는 되쓰기(reserve False) · `incomplete` 는 재구매(reserve True).
5. 새 정지 요청은 `cd210f76` 대로 스텝 밖으로 올라와 CP 를 안 쓴다.

**canary 쪽 둘.** (a) durable `completed` 만 보고 cap 0 을 주면 runner 의 RERUN_SELF 가 닫힌 문에 부딪힌다 → `completion_debt_of`(runner 의
`_safe_verify_completion` 로 묻는다) → `plan_reentry(debt=)` 정상 cap · dry 의 `reopened_for_debt`. (b) 코드가 다시 연 중앙(지문 force · 빚)에
검색·받기 문이 0 이던 것(dry 실측) → 다시 연 셋을 합쳐 문 재산정.

**단계명 갈라 적음 (Codex 요청).** 「dry 가 의무 20 을 선언 5 로 잡은 과소계측」(계약 결함, `assert_central_cap_covers` 로 닫음)과 「실제 2판 호출은
ca.run 안쪽 cap 32 안에서 정지 요청으로 끝남(cap 미도달)」은 다른 사건이다.

**live 4판(plain resume, 05:52).** 잴 것: 7 selected 되쓰기(provider 0) · 9 만 구매 · 하류 야외 CP 무효화/재투영 · supplement 재구매 0 ·
projection sha 동일 · 마지막 같은 run 재개 provider 0.


### §8 정정 다섯 — 통합 canary `f7cc45c576c0` (2026-09-03 06:21~07:55)

**판정(Codex).** 배경→scene_detail 통합 **통과** · 야외 보충 독립 통과(4398a55dc0bb) · **한 run 야외 통합은 미측정** — `scene_detail` 의 의존 닫힘에
야외 lane(outdoor_place_spec · lane_plan · structure_form_reference)이 없다(실측: step_run 도 CP 도 없음). 새 유료 run(≈150콜)은 열지 않는다.

**서고 고친 다섯.** ① 엔티티 큐가 (name, type) 로 접혀 같은 이름의 다른 실체(LP05·LP07)가 하나가 됨 → 신원은 producer 의 short_id(entity_detail →
entity_t2i → sync) · 팩 v15 표식 echo · 두 스텝 지문에 계약 결속 ② 제가 팩 버전을 lexical 정렬로 오독해 만든 v10 이 안 실림 → v14 위에 v15 · 진짜 loader 시험
③ sync 의 pre-pass NULL + (type,name) dict 가 동명 둘을 한 행으로 → 원래 SID→행 지도 · 후보 목록 · legacy 단일 후보만 · PostgreSQL 끝점 다섯
④ floor_plan_render 의 스텝 문이 0(이미지 호출도 스텝 문을 지난다) → 단위마다 3 ⑤ plain resume 가 scene_detail 을 매번 재구매 — episode_reference_policy 결과에
config_hash 가 없어 저장(project_config 지문)·비교(step-local 지문)가 어긋나 매 재개 force → 하류 stale → 결과에 config_hash · dry 의 forced_for_drift 는 적용 스텝 전부.

**재개 idempotency.** ResumeDecision 이 claim 전 행(prior_status·run_id·updated_at)을 싣고 recovery 로그는 그것만 적는다. 수리 뒤 연속 plain resume 세 번:
counted 0 · provider 0 · RECOVERY 0 · status 변화 0(run_id 는 not_applicable 9행에서 재판정으로 바뀜).

**outbound 문.** 「중앙이 끝났나」가 아니라 **닫힘 안 소비자 중 남았거나 다시 여는 것**(계약 `OUTBOUND_CONSUMER_STEPS`, app.core 를 안 올리는 가벼운 모듈 —
스텝 모듈에서 가져오자 자물쇠 전 import 로 IsolationRefused) · 어느 소비자로 열렸는지 기록 · AST 잠금.

### §8 정정 여섯 — 한 run 야외 통합 (같은 run `f7cc45c576c0` · target seed · 2026-09-03 09:02~09:30 · Codex 09:20·09:27·09:30)

**무엇을 쟀나.** 08:00 판정에서 「미측정·별도」로 남긴 것 — 이 run 의 중앙 CP(15/15)를 앞으로 둔 21.915 가 실제로 그 CP 를 소비하고, 그 산출을 seed(21.92)가 실제로 물고 그리는가. target 만 `outdoor_structure_seed` 로 바꿔 같은 run 에서 이었다(전이 6573acc25bb8 · c4289defe344).

**결과 (attempt 3 completed · acceptance A~F overall_ok).**
- 보충: 중앙에 `structure_form` 줄 0 → 21.915 가 `ca.run`(같은 공장·`journal_supplement.json`)으로 L02#structure_form 을 샀다(1판 5콜 · closest · 2라운드 8장). force 재실행(2판)은 journal 재생으로 **provider 0**.
- 투영 → 자산 → seed: `form_ref_path`/`form_ref_sha256`(9bcfe4f9…)/`final_id L02` + **`form_ref_asset_id a34cdc4f`** ↔ `image_asset` 행(project·episode·`structure_form_ref`·`bg_alley`·`form_ref` · 파일 sha 동일) ↔ seed CP `form_reference_input` {a34cdc4f, 9bcfe4f9} ↔ seed 자산 636a7867 `input_image_ids=[a34cdc4f]`. 양방향으로 닫혔다.
- 돈: 글 누계 243/480 · 이미지 **장부 차감 7/18**(floor_plan 4 + seed 3) vs **실제 전송 7 = 성공 3 · 오류 4** — 둘을 섞지 않는다(앞 판정문의 「이미지 1/18」은 completed attempt 의 성공만 센 것이었다 · append-only 정정).

**서고 고친 자리 셋.**
1. **지문 어긋남의 force 연쇄** — 팩 지문 결속(d6f9cac4·22524359)으로 `entity_detail` hash 가 움직였고, canary 는 어긋난 스텝을 force 로 돌리는데 runner 의 force 는 하류 59 스텝 CP 를 지운다. dry 는 「살 자리 5」만 보여 줬다. 첫 수리(범용 `drift_ack` → RERUN_SELF · 하류 보존)는 Codex 가 막았다: 다시 도는 스텝이 LLM 을 부르면(entity_steps:1240) 보존한 하류는 다른 계보다. 답은 **정확한 hash adoption**(`tools/grounding_audit/canary_hash_adoption.py`): tuple(step · old/new hash · resolve_effective raw_content_hash · data digest · manifest 신원 · identity_contract · 만든 attempt/tip · 팩 dir diff 0 · at_tip)이 정확히 맞을 때만 백업 → config_hash 메타데이터만 교체 → data digest 재확인 → runner 재확인 → append-only 사건(8cd4198bba62). dry 는 이제 force 마다 **지울 하류 목록**을 찍는다(`forced_would_invalidate_of`).
2. **투영이 자산을 안 묶었다** — legacy 라운드만 `_bind_ref_asset` 를 했고 투영은 path·sha 만 실어 seed 가 fail-closed(1판). `_bind_projected_asset`: uuid5(project·episode·group·location·sha256) 로 insert-or-verify · 그룹 commit · `FRONT_PROJECTION_CONTRACT_VERSION` 2.202609030915.
3. **provider 전송 오류** — 2판 seed 첫 롤이 「upstream connect error … connection termination」(SDK/router 재시도 0 잠금이 의도대로 막음). 코드 결함 아님 · Codex 승인 뒤 plain 재개 1판(GO 값 18/5/13 을 attempt 행과 같게 기록).

**남은 것.** §6.5 는 2026-09-03 10:20 사용자 판단으로 마감 — 「사용자 판단: 현재 결과는 잘 되는 것으로 보이며 추가 수동 평가 없이 마감」 · 갤러리·고증 화면은 선택적 후속/보존된 감사 자료 · production HITL 요구 0 · 미실시를 실패나 BLOCK 으로 해석하지 않음(A/B 승패는 적지 않는다). **범위 밖 후속(핵심 완료 범위 아님):** unverified 계측 단위 3(background_render · shot_continuity · shot_conti_light — seed 닫힘 밖 · 읽어 둔 사실은 HANDOFF ⑥) · adoption 사건의 two-phase(NON-BLOCK) · 이미지 문 셈에서 crashed attempt 의 예약 차감 vs 실제 전송 구분 표기.

