# Background Semantic Extractor Experiment — plan (W0, rev W0g)

> 상태: 작성자 = claude / 검토 = codex (W0 → W0b → W0c → APPROVED_FOR_W0_5 → W0.5 cleanup PASS → W1 implementation 보류 → W0d 폐기 → W0e → W0f patch → NEEDS_REVISION_BEFORE_W1 → W0g patch 후 재검토 대기). 코드 (W0c 8-category 기반 skeleton `backend/scripts/experiment_background_semantic_extractor.py` — Codex W0e BLOCKING 2 명시 **폐기 대상**, W0g APPROVED 후 §14 cleanup → W1 새 script W0e/W0f/W0g 2-stage contract 기준으로 새 작성) / 이미지 0 / API 0 / DB write 0. production 미수정.
>
> W0f → W0g 변경 (Codex 2026-05-24 NEEDS_REVISION_BEFORE_W1, BLOCKING 1 + IMPORTANT 3개 + MINOR 1):
> - BLOCKING — `stage_b_no_irrelevant_leakage` 의 substring/content 비교 제거. 사용자 standing rule (글자 패턴 인지 금지) + §6-A-4 "no brief content scan" 과 충돌. checker §3 #10 을 `stage_b_input_contract` 로 교체 — Stage B prompt/input object 의 **structural** 검사만 (full Stage A dict 없음 / `irrelevant_or_do_not_pass_down` key/list 없음 / `world_background_brief` embedded dict 없음 / 전달은 `world_brief_ref` + `world_hints_for_background` 만). 의미적 plot/lore 재누출은 W2 LLM/human review.
> - IMPORTANT 1 — §3 헤더 "다음 8 항목" → 정확한 카운트 (W0f 추가 후 10 항목). §4 / §5 의 "8 checker" 도 갱신.
> - IMPORTANT 2 — cap PASS (Codex 명시).
> - IMPORTANT 3 — skeleton 삭제 승인 (단 본 BLOCKING 닫기 전 §14 cleanup / W1 진입 X).
> - MINOR — `base_plate_role.identity_anchor` 설명 "반드시 base plate 에 흔적" → "base plate should be informed by this anchor" 로 완화 (직접 inventory화 회귀 위험).
>
> W0e → W0f 변경 (Codex 2026-05-24 NEEDS_REVISION_BEFORE_W1):
> - BLOCKING 1 — minimal 을 schema/checker 로 실제 보장. 배열 길이 cap + 문장 길이 cap 명시 (world_hints max 5/item 120chars, continuity_groups max 3, essential_spatial_relations max 5/desc·why 180chars, visual_anchors max 5, state_variations max 5, base_plate_prompt_brief max 900chars, generation_notes arrays max 5/item 160chars). checker 초과 시 fail. shape/minimality 검증이라 standing rule 위반 X.
> - BLOCKING 2 — W0c 기반 skeleton `experiment_background_semantic_extractor.py` 폐기. patch 가 아니라 사실상 새 작성. plan §4 / §5 의 stale `semantic_extraction.json` 참조 모두 정리. §14 cleanup-style 단락 추가.
> - BLOCKING 3 — Stage A → Stage B leakage guard 를 W1 dry-run template 에서도 보장. `prompt/spatial_user.txt` 가 full Stage A dict / `irrelevant_or_do_not_pass_down` list 를 embed 하지 않음 (placeholder 또는 actual `world_hints_for_background` short list 만). test 추가: spatial prompt template 에 Stage A full schema/response embedding 없음, irrelevant list 전달 없음.
> - IMPORTANT 1 — Stage A world fields 5개 유지 (lighting/scale/weather baseline 추가 X). 필요시 short hints 안에.
> - IMPORTANT 2 — `count` 금지는 JSON key 이름 기준만. source quote 안 숫자/표현 ("두 개" 등) 은 provenance 데이터 허용. checker 가 output JSON key 이름 `count` 만 거부. plan/test 에 명확히.
> - IMPORTANT 3 — `must_appear_in_base_plate: true` → `base_plate_role: identity_anchor | optional | avoid` 완화. rigid inventory 회귀 위험. base plate = identity/scale/layout feel anchor 만.
> - MINOR — §6 헤더 "17 test" → "23 test" (W0e 21 + W0f BLOCKING 3 W1 template guard 2 추가). 13 output files 유지 (minimal 은 파일 수 아니라 payload/brief 크기 기준).
>
> W0d → W0e 변경 (사용자 2026-05-24 지적):
> - 사용자 핵심: "너무 디테일한 정보 추출은 불필요. T2I/I2I 가 그 디테일 제대로 반영하거나 이해한다고 보장 못 함. 배경 이미지를 멋지게 만들어도 샷 이미지 생성 단계에서 엔티티 결합/공간 이해가 떨어져 결과가 더 나빠질 수 있음. 지금은 단순한 방식으로 시작하고, 이후 조금씩 개선."
> - W0d 9-category 폐기. W0c 8-category 도 폐기. 산출 schema 를 **단일 `MinimalSpatialBrief`** 로 축소.
> - 목적 shift — "semantic extraction" → "minimal spatial brief". T2I/I2I 친화 짧은 brief, 공통 identity / scale / layout feel anchor, base plate prompt brief + shot background rules + avoid over-specification 중심.
> - "무엇을 일관되게 유지" vs "상태/카메라에 따라 바뀌어도 됨" 분리 (continuity_groups / state_variations).
> - category 적게 / 문·창·가구·개수·detailed inventory 강제 금지 / `count` field 금지.
> - prompt 갱신 — "keep brief compact / avoid over specification / do not inventory objects or count doors/windows/furniture / do not assume the location is a house, room, set, building, or interior".
> - checker §3 단순화 — schema required fields / evidence_refs non-empty / source_ref resolvable / quote containment / no topology-final fields / no `count` required field / reasoning text 내용 검사 0.
> - W1 test 33 → **15~18** 로 축소.
> - W0d 헤더에 추가했던 9-category 요약은 본 §2 본문 갱신으로 폐기 (헤더는 이력 남김).
>
> W0e 추가 반영 (사용자 2026-05-24 추가 지적 "또 다른 중요한 world 에 대한 초기 자료로 이미지 생성시에 상황을 설명해야해"):
> - **world 정보 필요. 단 MinimalSpatialBrief 안에 통째 끼우지 말 것.**
> - 사용자 추가 정정 "그대로 추가하면 안되, 배경에 필요한 부분만 다시 LLM을 거쳐서 사용해야해" → **2-stage LLM flow** 로 재설계.
> - **Stage A — `BackgroundWorldBrief` 추출**: 짧은 world grounding (era / geography / culture / tech / socioeconomic / genre) 만 추출하고 background-irrelevant plot/lore 는 명시적으로 `irrelevant_or_do_not_pass_down` 으로 버림.
> - **Stage B — `MinimalSpatialBrief` 추출**: Stage A 결과의 `world_brief_ref` + 짧은 `world_hints_for_background` 만 참조. world_context_brief 전체 embedding 금지.
> - W1 dry-run = 두 Stage 의 prompt/schema/placeholder 모두 emit (LLM 호출 0). W2 generate = Stage A 호출 → Stage B 호출 (sequential).
> - 산출 파일 9 → 13 (world_brief_schema.json / prompt/world_system.txt / prompt/world_user.txt / world_brief.json / minimal_spatial_brief.json — semantic_extraction.json 대체).
> - prompt 2개 분리: world prompt = "Extract only background-generation-relevant world grounding. Do not pass plot lore unless it changes visual background assumptions." / spatial prompt = "Use only the compact world hints from Stage A, not the full source world analysis."
> - checker = 두 schema 모두 검증, evidence_refs / source_ref / quote containment / no count / no topology.
> - test 17 → 21 (2-stage 추가).
>
> W0b → W0c 변경 (Codex 2026-05-24 NEEDS_REVISION_BEFORE_W0_5):
> - BLOCKING 1 — §13 cleanup 범위 모순 해소: `main()` 에 추가된 `load_selected_shots(..., loc_short_ids=[spec.location_short_id])` 는 W1e aborted 변경이라 **revert 대상**으로 명확화. "W1d BLOCKING 2 fix" 표현 삭제 — W1d 는 shot scoping fix 가 아니라 readiness state_model + metric key rename fix 였음. §13-C 보존 대상 = W1d approved 항목만.
> - IMPORTANT 1 — §6-A 정적 가드 test 가 raw source string scan 으로 보이는 표현 제거: AST/name-level structural guard 로 재작성. `FORBIDDEN_WORDS`/`DENYLIST` 같은 module-level constants 부재 + checker 함수 AST 안에 reasoning_basis 대상 `in`/`.find`/`.contains`/`for word in deny_list` 구조 부재 만 확인. raw text grep 금지.
> - IMPORTANT 2 — §3 / §11 등에 남아있던 "exact substring" 용어 제거. "exact quote containment" / "verbatim quote containment" / "source text contains the exact quote" 로 통일. 과거 회귀 문맥 (W1~W1e literal substring 매칭 폐기) 설명 외에는 substring 단어 안 씀.
> - IMPORTANT 3 — §6-A 정적 가드 검사 범위 명시: runtime checker / source bundle builder / semantic extractor engine 함수 범위만. prompt template 문자열 (system.txt / user.txt) 검사 제외 — 그 안에 "pattern/term/boundary 금지" 문구가 정상적으로 들어감.
>
> W0 → W0b 변경 (Codex 2026-05-24 NEEDS_REVISION):
> - BLOCKING 1 — runtime forbidden-word checker (`no_pattern_reasoning` reasoning_basis word scan) 제거 (§3, §6). 패턴 금지 원칙을 패턴 검사로 enforce 하면 같은 회귀. 정적 가드는 "script 본문에 lexicon matcher/classifier function 없다" + "no `re` import" 만.
> - BLOCKING 2 — selected_shots loader 가 `visible_entities_json` JSON parse 후 entity object 의 `short_id == spec.location_short_id` exact equality 만 사용 (§1, §6). text substring 매칭 금지.
> - BLOCKING 3 — W0.5 cleanup wave 추가 (§13). aborted W1e literal-boundary diff (코드/test/plan §8-G) 정리. approved W1b/W1d snapshot 산출은 보존.
> - IMPORTANT 1 — token 한도: W1 truncate/drop/chunk 금지, char/token budget 표시만, W2 generate fail-closed (§5, §11).
> - IMPORTANT 2 — model default = placeholder `model_for_future_generate`, W2 전 재검증 (§8, §9).
> - IMPORTANT 3 — candidate_kind free string 유지 (§2, §10).
> - IMPORTANT 4 — char_start/end optional 유지, checker 가 quote occurrence 보강 (provenance augmentation) (§2, §3, §6).
> - IMPORTANT 5 — `--include-diagnostic` default off, HTML 에 "deprecated diagnostic input" 명시 (§1, §4, §5).
> - MINOR — `re` import 자체 금지 (slug 검증도 char loop) / "substring" 용어 → "exact quote containment / provenance check" 통일 / §8 모델명 candidate only (§6, §8).
>
> 직전 wave `background_topology_planner_experiment` (W1~W1d literal lexicon 기반 + W1e literal-boundary patch 중단) 는 사용자 두 차례 강력 질책 + Codex 권고로 SOT 폐기. 본 plan §13 cleanup 가 그 diff 를 정리. 본 실험은 사용자 지시 "이제 전면 재시작해 실험용 코드!!!!" 로 새 출발.
>
> 본 plan 의 목적: **literal/regex/substring/term-boundary/조사/글자 단위 패턴 인지 없이**, LLM 이 source bundle 을 읽고 evidence-backed semantic structured extraction 을 수행할 수 있는지 W0 plan 으로 설계 → W0.5 cleanup → W1 dry-run → W2 generate. 본 W0 = plan 만, 코드 0.

---

## ★★★ 절대 규칙 (2026-05-24 사용자 standing rules)

1. **패턴 인지 / regex / 글자 substring 매칭 / term boundary / 조사 / 글자 단위 의미 구분 금지** ([[feedback-no-literal-substring-meaning]]). literal lexicon matcher 의 *어떤 형태도* — primary, audit 보조, smoke check, coverage hint, SOT 비교 — 살리지 않음. **이 원칙을 deterministic code 의 forbidden-word scan 으로 enforce 하는 것도 금지** (W0b BLOCKING 1) — 그 자체가 패턴 검사라 같은 회귀. prompt 가 LLM 에 지시하고 정적 가드 (AST/name-level: lexicon matcher/classifier function 부재 + `FORBIDDEN_WORDS`/`DENYLIST` constant 부재 + `re` import 부재) 로만 enforce. quote provenance 검증 (LLM 이 낸 quote 가 source 본문에 verbatim containment — source text contains the exact quote) 은 의미 분류가 아니라 출처 확인이라 허용.
2. **특정 시나리오 (L05/옥탑방/특정 방·캐릭터·prop·장면 문구) 기반 일반 rule/prompt/code 금지** ([[feedback-no-scenario-specific-coding]]). fixture data 는 `SAMPLE_FIXTURE_*` / `SampleFixtureSpec` 격리. generic schema/contract/code 만.
3. **production code 수정 금지** — experiment script + tests + plan + scripts_output/ 만.
4. **DB read-only (PostgreSQL only)** ([[project-sqlite-not-used]]). DB write 0. image 0. openai/google.genai/google.generativeai/fal/PIL 같은 외부 호출 모듈 import 자체는 OK 단 `--generate` default off 라 실제 호출은 wave 분리.
5. **모든 subagent = Opus 4.7** ([[feedback-subagent-always-opus]]).
6. **HTML 산출은 외부 http server 만** ([[feedback-html-artifact-serve-external]]) — `open <path>` 금지.

---

## 0. 핵심 원칙 (semantic-first SOT)

1. **literal lexicon matcher 폐기.** Primary extraction = LLM 의 의미 단위 분류.
2. **evidence-backed.** 모든 LLM 산출 item 에 quote + source_ref + confidence + reasoning_basis 필수. quote 가 source 본문에 verbatim 으로 존재해야 함 — source text contains the exact quote (provenance check).
3. **deterministic code 는 provenance 와 shape 검증만.** 의미 분류·필터링·매핑·우선순위는 100% LLM 결과 신뢰. **deterministic code 안에 regex/substring/lexicon/Hangul boundary/조사 list/forbidden-word scan 등 글자 패턴 코드 자체 0** (정적 가드).
4. **dry-run first.** `--generate` default off. W1 = prompt/schema/source bundle/checker plan + test. 실제 LLM 호출은 W2.
5. **topology 확정 X.** 본 wave 는 evidence-backed **semantic cards** 만 emit. topology graph / background unit / image / decision ledger 는 별도 후속 wave.
6. **fixture swap 가능한 generic engine.** sample fixture L05 외 다른 fixture 추가해도 schema / prompt / checker 그대로.
7. **selected_shots scope = structured JSON exact equality** — substring 회귀 방지 (BLOCKING 2).
8. **source truncate/drop/chunk 금지** — W1 은 char/token budget 만 표시. W2 generate 시 context 초과면 fail-closed (`source_too_large_for_model`).

---

## 1. SourceBundle contract

source_bundle 은 LLM input 의 SOT. 모든 source 는 deterministic ref + sha256 hash + char_count 로 식별.

```json
{
  "bundle_id": "bsx_<run_id>",
  "fixture_id": "<sample fixture id>",
  "project_id": "<uuid>",
  "episode_id": "<uuid>",
  "location_short_id": "<L##>",
  "char_budget_estimate": <int>,
  "sources": [
    {
      "source_ref": "planning_doc:<project_id>",
      "kind": "planning_doc",
      "text": "<full planning doc body>",
      "sha256": "<hex>",
      "char_count": <int>
    },
    {
      "source_ref": "episode_fulltext:<episode_id>",
      "kind": "episode_fulltext",
      "text": "...",
      "sha256": "...",
      "char_count": <int>
    },
    {
      "source_ref": "shot:<still_id>",
      "kind": "selected_shot",
      "text": "<shot_description + scene_summary + visible_entities_json 합본>",
      "scene_index": <int>,
      "shot_index": <int>,
      "still_id": "<uuid>",
      "loc_short_ids_visible": ["L##"],
      "sha256": "...",
      "char_count": <int>
    },
    {
      "source_ref": "entity:<canon_id>",
      "kind": "entity_catalog",
      "entity_type": "character|prop|outlook",
      "name": "...",
      "text": "<name + description + metadata_json 합본>",
      "sha256": "...",
      "char_count": <int>
    },
    {
      "source_ref": "location:<canon_id>",
      "kind": "location_catalog",
      "short_id": "L##",
      "name": "...",
      "text": "...",
      "sha256": "...",
      "char_count": <int>
    },
    {
      "source_ref": "artifact:<relative_path>",
      "kind": "existing_artifact",
      "text": "<JSON pretty-printed>",
      "sha256": "...",
      "char_count": <int>,
      "deprecated_diagnostic": true
    }
  ]
}
```

규칙:
- **selected_shots scope (BLOCKING 2)**: loader 는 DB 의 `scene_still` 을 모두 조회한 뒤, 각 row 의 `visible_entities_json` 을 `json.loads()` 로 parse. parse 결과가 list 면 각 element 가 dict 이고 `element["short_id"] == spec.location_short_id` exact equality 일 때만 shot 포함. parse fail / 형식 mismatch row 는 제외 + missing_inputs 에 기록. **text substring 매칭 절대 금지** (직전 W1d 의 `if short in ve` 형태도 회귀라 폐기).
- 각 source 는 `source_ref` 가 unique key. LLM 의 `evidence_refs.source_ref` 와 1:1 매칭.
- `text` 는 LLM 에 그대로 전달 (CLAUDE.md "절대 규칙: LLM 에 전달하는 데이터 절대 자르지 마라" 준수). **truncate/drop/chunk 금지** (IMPORTANT 1). W2 generate 시 context 초과 추정되면 fail-closed.
- `sha256` + `char_count` 로 bundle determinism + manifest 무결성 검증.
- `char_budget_estimate` = 모든 source `char_count` 합 + prompt overhead. W1 run_meta + HTML 에 표시 — 사용자가 한눈에 prompt token risk 인지.
- `existing_artifact` 는 optional, `--include-diagnostic` flag 있을 때만 (default off, IMPORTANT 5). 포함 시 `deprecated_diagnostic=true` marker 로 HTML 가시화 — 기존 literal/pattern 계열 artifact 가 새 semantic extractor 를 오염시키지 않게 경고.

---

## 2. LLM extraction schema — 2-stage (W0e, 사용자 추가 정정 반영)

W0e 산출은 **2-stage LLM flow**. Stage A 가 world 정보를 압축/필터링 → Stage B 가 그 결과의 background-relevant hints 만 참조. world 정보를 MinimalSpatialBrief 에 통째 끼우지 않음 — 별도 LLM 단계에서 "이미지 생성에 필요한 world context만 남기고 나머지는 버리는 것".

### 2-A. Stage A — `BackgroundWorldBrief` schema

```json
{
  "schema_version": "bsx_world_w0e",
  "bundle_id": "<bsx_run_id>",
  "world_background_brief": {
    "era_and_time_period": "<max 160 chars, e.g. present-day | near-future 20XX | 1990s | unknown — generic>",
    "geographic_cultural_grounding": "<max 200 chars, e.g. coastal South Korean island village | unknown — generic>",
    "technology_and_material_baseline": "<max 200 chars, e.g. contemporary mobile-era, traditional fishing tools | unknown — generic>",
    "social_economic_visual_tone": "<max 200 chars, e.g. working-class small-town, weathered surfaces | unknown — generic>",
    "genre_mood_constraints": "<max 220 chars, e.g. occult thriller with ritual undertones — affects palette/lighting, not luxury | unknown — generic>",
    "background_relevant_do_not_assume": [
      "<max 5 items, each max 160 chars>"
    ],
    "irrelevant_or_do_not_pass_down": [
      "<max 8 items, each max 200 chars — plot fact / character relationship / mythology lore / event chronology that does NOT change visual background; MUST NOT pass into Stage B>"
    ],
    "evidence_refs": [<EvidenceRef>]
  },
  "confidence_band": "trusted | plausible | weak | unknown"
}
```

Stage A 목적: 미래/한국/시대/문화권/사회경제 톤/기술 수준 중 **배경 시각에 영향을 주는 것만** 추출. 인물 관계 / 플롯 / 사건 진상 / 신화 설명 같은 background-시각 무관 정보는 `irrelevant_or_do_not_pass_down` 에 명시하고 Stage B 에 전달하지 않음. 짧은 context only.

**Stage A 5 시각 필드 유지** (Codex W0e IMPORTANT 1) — lighting/scale/weather baseline 추가 X. 필요시 5 필드 본문 안 또는 Stage B short hints 안에 짧게.

### 2-B. Stage B — `MinimalSpatialBrief` schema

Stage A 의 BackgroundWorldBrief 를 input 으로 받되 그 결과의 짧은 hints 만 참조. world_context_brief 전체 embedding 금지.

```json
{
  "schema_version": "bsx_minimal_w0e",
  "bundle_id": "<bsx_run_id>",
  "world_brief_ref": "world_background_brief",
  "world_hints_for_background": [
    "<max 5 items, each max 120 chars — short background-relevant hint only; MUST NOT contain plot/lore from Stage A irrelevant_or_do_not_pass_down>"
  ],
  "place_identity": {
    "label": "<max 80 chars, short human label, generic — any place type OK>",
    "one_sentence": "<max 240 chars, one-sentence description, T2I-friendly>",
    "evidence_refs": [<EvidenceRef>]
  },
  "continuity_groups": [
    {
      "group_id": "<slug, max 60 chars>",
      "label": "<max 80 chars>",
      "what_must_stay_consistent": ["<max 5 items, each max 80 chars>"],
      "allowed_variations": ["<max 5 items, each max 80 chars>"],
      "evidence_refs": [<EvidenceRef>]
    }
  ],
  "essential_spatial_relations": [
    {
      "relation_id": "<slug, max 60 chars>",
      "description": "<max 180 chars — containment/connection/access/visibility/scale, no door/window count>",
      "why_it_matters_for_generation": "<max 180 chars, free string>",
      "evidence_refs": [<EvidenceRef>]
    }
  ],
  "visual_anchors": [
    {
      "anchor_id": "<slug, max 60 chars>",
      "description": "<max 180 chars, generic>",
      "role": "<free string, suggested labels: orientation | scale | repeated landmark | surface | access | atmosphere>",
      "base_plate_role": "identity_anchor | optional | avoid",
      "evidence_refs": [<EvidenceRef>]
    }
  ],
  "state_variations": [
    {
      "state_id": "<slug, max 60 chars>",
      "description": "<max 180 chars, generic>",
      "changes_only": ["<max 5 items, each max 120 chars>"],
      "must_not_change": ["<max 5 items, each max 120 chars>"],
      "evidence_refs": [<EvidenceRef>]
    }
  ],
  "generation_notes": {
    "base_plate_prompt_brief": "<max 900 chars, T2I-friendly paragraph — identity/scale/layout feel anchor only, NOT every shot's image>",
    "shot_background_prompt_rules": ["<max 5 items, each max 160 chars>"],
    "avoid_over_specification": ["<max 5 items, each max 160 chars>"],
    "unknowns_to_keep_loose": ["<max 5 items, each max 160 chars>"]
  },
  "confidence_band": "trusted | plausible | weak | unknown"
}
```

**Array cap (Codex W0f BLOCKING 1)**: `continuity_groups` max **3** / `essential_spatial_relations` max **5** / `visual_anchors` max **5** / `state_variations` max **5** / `world_hints_for_background` max **5**. checker 가 초과 시 fail. 이는 minimality 의 schema-shape 보장이며 의미 분류 아님.

**`base_plate_role` enum 3개** (Codex W0f IMPORTANT 3 완화 + W0g MINOR 추가 완화) — W0e 의 `must_appear_in_base_plate: true` 폐기. base plate 가 identity/scale/layout feel anchor 만 그리므로 모든 anchor 를 base plate 에 강제 X. enum: `identity_anchor` (base plate should be informed by this anchor — 직접 물체 inventory 가 아니라 base plate 설계 시 참고 의미), `optional` (있어도/없어도), `avoid` (base plate 에 그리지 않음 — 상태 의존).

`<EvidenceRef>` (단순화):

```json
{
  "source_ref": "<must match a SourceBundle source_ref>",
  "quote": "<verbatim — source.text contains the exact quote>",
  "char_start": <optional int>,
  "char_end": <optional int>,
  "confidence": "trusted | plausible | weak | unknown"
}
```

### 2-C. 규칙

- **Stage A `world_background_brief`**: 5 시각 필드 (era_and_time_period / geographic_cultural_grounding / technology_and_material_baseline / social_economic_visual_tone / genre_mood_constraints) 각 generic free string. 모르면 `"unknown"`. `background_relevant_do_not_assume` + `irrelevant_or_do_not_pass_down` 두 list 의무. `evidence_refs` ≥ 1.
- **Stage B `world_brief_ref` + `world_hints_for_background`**: Stage A 결과를 참조만. world_context_brief 통째 embedding 금지 (test 가 enforce). `world_hints_for_background` 는 짧은 short string list (item 별 1-2줄). Stage A 의 `irrelevant_or_do_not_pass_down` 에 있는 내용이 hints 에 등장하면 안 됨.
- **Stage B 의 brief 항목 (place_identity / continuity_groups / essential_spatial_relations / visual_anchors / state_variations) 은 `evidence_refs` ≥ 1 강제.** `world_brief_ref` / `world_hints_for_background` / `generation_notes` / top-level `confidence_band` 는 evidence_refs 불요.
- **두 단계 모두 단일 객체 산출.** "candidate list of N items per category" 아님 — minimal 핵심만.
- **`count` field 금지** (top-level 또는 any 항목 안). schema 에 `count` required field 없고, LLM 출력에 `count` 등장 시 checker fail. 문/창/가구/오브젝트 inventory 강제 안 함.
- **공간 가정 금지**: place_identity 의 label / one_sentence / continuity_groups 등 모두 "any place type" (집·배·숲·골목·계단·마당·사무실·의례 장소 등) — schema 본문에 house/room/door/window/furniture 같은 주거 전제 명사 등장 X. 단 LLM 이 source 에서 evidence-backed 하게 그런 단어를 emit 하는 건 OK (source 가 그렇게 말하면).
- **`continuity_groups[].what_must_stay_consistent` 와 `allowed_variations`**: 예시 list 만 권고. LLM 이 source 에 맞게 free string 으로 채움.
- **`visual_anchors[].role`** enum 6개 권고. enum 강제 안 함 (free string OK, suggested labels only).
- **`state_variations[].changes_only` vs `must_not_change`**: T2I 생성 시 "어떤 aspect 가 바뀌어도 base identity 유지" vs "어떤 aspect 는 base 와 동일해야" 구분.
- **`generation_notes`**: T2I/I2I prompt 작성용 짧은 brief. base_plate ≠ 모든 샷 직접 그리기 — 공통 identity/scale/layout feel anchor 용도.
- **`char_start / char_end` optional**: LLM 강제 X — checker 가 quote 위치 보강 (provenance augmentation).
- **`item_id` (continuity_groups.group_id / essential_spatial_relations.relation_id / visual_anchors.anchor_id / state_variations.state_id)**: slug pattern (ASCII alnum + `_`/`-`) 을 char loop 로 검사 (re import 금지).

---

## 3. Deterministic checker contract (provenance & shape only — minimal, 2-stage, W0e)

checker 는 **의미 분류 0** + **forbidden-word scan 0** (W0b BLOCKING 1). 두 schema 각각 검증, generation_notes / world_brief_ref / world_hints_for_background 같은 composed/reference 필드는 evidence_refs 면제. 다음 10 항목:

1. **stage_a_schema_required_fields**: BackgroundWorldBrief top-level `schema_version / bundle_id / world_background_brief / confidence_band` 4 키. `world_background_brief` = `{era_and_time_period, geographic_cultural_grounding, technology_and_material_baseline, social_economic_visual_tone, genre_mood_constraints, background_relevant_do_not_assume, irrelevant_or_do_not_pass_down, evidence_refs}` 8 키.
2. **stage_b_schema_required_fields**: MinimalSpatialBrief top-level `schema_version / bundle_id / world_brief_ref / world_hints_for_background / place_identity / continuity_groups / essential_spatial_relations / visual_anchors / state_variations / generation_notes / confidence_band` 11 필수 키. `place_identity` = `{label, one_sentence, evidence_refs}` 3 키. 각 list 항목 schema 명시 필수 필드.
3. **stage_a_evidence_refs_non_empty**: `world_background_brief.evidence_refs` ≥ 1.
4. **stage_b_evidence_refs_non_empty**: `place_identity` + `continuity_groups[]` + `essential_spatial_relations[]` + `visual_anchors[]` + `state_variations[]` 각 항목의 `evidence_refs` length ≥ 1. world_brief_ref / world_hints_for_background / generation_notes / top-level confidence_band 는 제외.
5. **source_ref_resolvable**: 두 schema 모두 `evidence_refs.source_ref` 가 SourceBundle source list 안에 존재.
6. **quote_exact_containment**: 두 schema 모두 각 `evidence_refs.quote` 가 해당 `source_ref` 의 `text` 안에 verbatim 으로 존재. 발견된 occurrence 위치 (char_start, char_end) 를 validation_report.augmented_occurrences 에 기록.
7. **confidence_enum**: 두 schema 의 top-level `confidence_band` + 각 evidence_refs.confidence ∈ {trusted, plausible, weak, unknown}.
8. **no_forbidden_fields**: schema-shape guard. 다음 필드가 두 schema 의 top-level / item-level 어디든 등장 시 fail:
   - topology 확정 필드: `parent_place_group_id` / `set_topology_id` / `geometry_change_kind` / `bound_unit_need_ids` / `structural_version_id` / `rollup_id` / `decision_status`.
   - `count` 단독 키 이름 — **JSON key 이름만 거부** (Codex W0f IMPORTANT 2). source quote 본문에 숫자나 "두 개" 같은 표현이 나오는 건 provenance 데이터라 허용. checker 가 quote string 안 단어/숫자 grep 안 함.
   - Stage B 에 `world_context_brief` / `world_background_brief` 통째 embedding (전체 dict) — Stage A 결과는 `world_brief_ref` + `world_hints_for_background` 로만 참조. 두 stage 결과를 deterministic code 가 cross-check.
   - 필드 이름 enum 검사. 본문 단어 / reasoning text 검사 아님.

9. **minimality_caps** (Codex W0f BLOCKING 1 + W1b/W1c 사용자 추가 standing rule): 두 schema 의 array length + string length cap 검사. **cap 초과는 validation failure 로만 처리. checker / script 어디서도 brief content / source text 를 truncate / slice / drop / chunk / auto-shorten / strip / NFC normalize 하지 않음. 원문 보존 verbatim.** DB loaders (load_planning_doc / load_episode_fulltext / load_selected_shots_for_location) 는 `.strip()` 호출 0 — `None` 만 `""` 로 치환. `_make_source_item` 도 `unicodedata.normalize` 호출 0. validation_report 의 `cap_exceeded:*` 필드가 fail 기록. test 6-F-24/25/26/27/28 가 enforce.
   - Stage A: `world_background_brief.background_relevant_do_not_assume` len ≤ 5, item ≤ 160 chars; `irrelevant_or_do_not_pass_down` len ≤ 8, item ≤ 200 chars; 5 시각 필드 각각 max chars (era 160 / geography 200 / tech 200 / socioeconomic 200 / genre 220).
   - Stage B: `world_hints_for_background` len ≤ 5, item ≤ 120 chars; `continuity_groups` len ≤ 3; `essential_spatial_relations` len ≤ 5, description/why each ≤ 180 chars; `visual_anchors` len ≤ 5, description ≤ 180 chars; `state_variations` len ≤ 5, description ≤ 180 chars; `generation_notes.base_plate_prompt_brief` ≤ 900 chars; `generation_notes.*_arrays` (shot_background_prompt_rules / avoid_over_specification / unknowns_to_keep_loose) len ≤ 5, item ≤ 160 chars.
   - cap 초과 시 fail. **이는 minimality 의 schema-shape 보장, 의미 분류 아님** (Codex 명시).

10. **stage_b_input_contract** (Codex W0g BLOCKING — W0f 의 substring 비교 폐기, structural 만): Stage B prompt builder / Stage B input object 의 **구조** 만 검사. content/substring 비교 X. 다음 4 부정:
   - Stage B input 에 `world_background_brief` 전체 dict 가 embed 되어 있지 않음 (top-level key 부재).
   - Stage B input 에 `irrelevant_or_do_not_pass_down` key 부재 (Stage A 의 plot/lore list 가 Stage B 로 통째 안 넘어감).
   - Stage B prompt builder function signature 가 `irrelevant_or_do_not_pass_down` 또는 `world_background_brief` 같은 full Stage A field 를 parameter 로 받지 않음 (AST FunctionDef.args 검사).
   - Stage B 로 전달되는 input 의 world 관련 키 = `world_brief_ref` (string) + `world_hints_for_background` (list of string) 만. 이 외 world 계열 key 등장 시 fail.

   의미적으로 LLM 이 plot/lore 를 hint 에 다시 써넣는 경우는 deterministic code 가 잡을 수 없는 영역 — W2 LLM/human review 항목으로 두고, W1 checker 는 input contract structural enforcement 만 한다. Codex W0g BLOCKING 명시: "deterministic code 가 의미를 모르는 상태에서 문구 비교로 잡으려 하면 다시 패턴 검사 회귀".

정적 가드 (script body AST 검사 — 6-A 그룹):

- **no_re_import**: `import re` / `from re import` 0.
- **no_term_lexicon_or_boundary_constants**: module-level `ast.Assign` target name 에 `LEXICON / TERMS / NEGATION / PARTICLES / BOUNDARY / HANGUL / MIN_TERM_LENGTH / WORD_BOUNDARY / FORBIDDEN_WORDS / DENYLIST` 0.
- **no_classifier_function_definition**: function name prefix `classify_ / match_term_ / iter_term_ / is_valid_match_ / scan_forbidden_` 0.
- **checker_function_ast_has_no_content_scan**: checker 함수 AST 안에 brief item field 의 string content 를 `in` / `.find(` / `.contains(` / `.lower()` / `for word in deny_list` 로 검사하는 구조 0. `if not brief['place_identity']['label']:` 같은 boolean/falsiness 확인은 허용.

위 6 checker + 4 정적 가드 외 의미 분류·필터링·매핑·우선순위·forbidden-word scan 등 0. semantic 책임은 100% LLM, deterministic code 는 provenance + shape 만.

---

## 4. CLI

```bash
PYTHONPATH=backend python backend/scripts/experiment_background_semantic_extractor.py \
  --output-root scripts_output/background_semantic_extractor_experiment \
  --run-id <optional, default YYYYMMDD_HHMM_<short_uuid>> \
  --project-id <optional override> \
  --episode-id <optional override> \
  --location-short-id <optional override, default = spec.location_short_id> \
  --include-diagnostic \
  --generate \
  --model <optional, default = placeholder; ignored when --generate off> \
  --dry-run \
  --no-serve \
  --serve-port <int>
```

- `--generate` default off. dry-run 모드는 prompt/schema/source bundle/checker plan + planned_validation 만 emit. LLM 호출 0.
- `--generate` on 시 LLM 호출 (Stage A → Stage B sequential) → `world_brief.json` + `minimal_spatial_brief.json` + 실제 `validation_report.json` (10 checker 모두 실행 — 1-8 provenance/shape + 9 cap + 10 stage_b_input_contract structural) + `source_too_large_for_model` fail-closed 검사 (W0b IMPORTANT 1).
- `--model` 은 W1 dry-run 에서 run_meta placeholder. W2 generate 시 실제 사용 가능 모델로 갱신 (IMPORTANT 2). 본 W1 default = `"model_for_future_generate"`.
- `--dry-run` 명시 시 `--generate` off 강제.
- `--no-serve` 시 http.server import / bind 0.
- `--include-diagnostic` default off (IMPORTANT 5). 사용 시 HTML 에 "deprecated diagnostic input" warning.
- `--project-id / --episode-id / --location-short-id` 가 fixture 와 다르면 자동 override mode.

---

## 5. Outputs (run dir: `scripts_output/background_semantic_extractor_experiment/<run_id>/`)

| 파일 | 내용 |
| --- | --- |
| `source_bundle.json` | §1 SourceBundle contract (deterministic, sha256 anchored, char_budget_estimate 포함) |
| `source_bundle.md` | 사람이 읽는 source summary |
| `world_brief_schema.json` | §2-A Stage A BackgroundWorldBrief JSON schema |
| `minimal_spatial_brief_schema.json` | §2-B Stage B MinimalSpatialBrief JSON schema |
| `prompt/world_system.txt` | Stage A system prompt — "Extract only background-generation-relevant world grounding. Do not pass plot lore unless it changes visual background assumptions." + 5 world 필드 정의 + irrelevant_or_do_not_pass_down 강조 |
| `prompt/world_user.txt` | Stage A user prompt — source bundle 전체 + BackgroundWorldBrief schema 인스트럭션 |
| `prompt/spatial_system.txt` | Stage B system prompt — "Use only the compact world hints from Stage A, not the full source world analysis." + "keep brief compact / avoid over specification / do not inventory objects or count doors/windows/furniture / do not assume the location is a house, room, set, building, or interior / extract spatial decomposition that works for any place type" |
| `prompt/spatial_user.txt` | Stage B user prompt — source bundle 전체 + (W2 generate 시) Stage A 의 world_hints_for_background + MinimalSpatialBrief schema 인스트럭션. W1 dry-run 시는 placeholder Stage A 결과 표기. |
| `world_brief.json` | `--generate` on 시 Stage A LLM response. off 시 `{ "placeholder": true, "stage": "world", ... }` |
| `minimal_spatial_brief.json` | `--generate` on 시 Stage B LLM response. off 시 `{ "placeholder": true, "stage": "spatial", ... }` |
| `validation_report.json` | checker 결과 — `--generate` off 시 두 schema 모두 `{ "planned_checks": [...], "executed": false }`. on 시 10 checker 결과 (1-8 + cap + input_contract) + augmented_occurrences |
| `run_meta.json` | run_id / plan_version / fixture_id / model placeholder / stages / args / outputs / sha256 / char_budget_estimate |
| `index.html` | 첫 화면: source summary + char_budget warning + 두 schema overview + 두 prompt preview + "why minimal + why 2-stage" explanation + planned/actual validation + diagnostic input flag |

총 13 파일 (W0c 9 → W0e 13, 2-stage 분리로 schema/prompt/response 각각 +2).

---

## 6. Tests (TDD invariants — W0g minimal + caps + leakage guard + no-truncate + verbatim source guards, 28 test)

**검사 범위 (W0c IMPORTANT 3):** 6-A 정적 가드는 *runtime checker / source bundle builder / minimal brief builder / prompt builder (signature 만, 본문 X)* AST 만 대상. **prompt template 문자열 (`prompt/system.txt`, `prompt/user.txt`) 은 검사 제외**.

### 6-A. Pattern-recognition ban guards (5)
1. `test_script_has_no_re_import` — AST Import/ImportFrom 검사.
2. `test_script_has_no_term_lexicon_or_boundary_constants` — module-level assignment name 검사.
3. `test_script_has_no_classifier_function_definition` — function name prefix `classify_ / match_term_ / iter_term_ / is_valid_match_ / scan_forbidden_` 0.
4. `test_checker_function_ast_has_no_brief_content_scan` — checker 함수 AST 에 brief item field string content 를 `in`/`.find(`/`.contains(`/`.lower()`/loop over deny_list 로 검사하는 구조 0.
5. `test_checker_unit_passes_reasoning_text_with_pattern_words` — checker 를 직접 호출해 brief description/one_sentence 등에 "regex boundary literal substring 조사" 같은 단어가 들어가도 schema 통과면 OK 회귀 증명.

### 6-B. SourceBundle integrity (3)
6. `test_source_bundle_selected_shots_scoped_via_json_exact_equality` — loader 가 `visible_entities_json` 을 `json.loads()` 후 dict `short_id` exact equality 로만 필터 (substring 매칭 코드 0).
7. `test_source_bundle_each_source_has_ref_text_sha256_count` — 모든 source 4 필수 필드.
8. `test_source_bundle_char_budget_estimate_present` — bundle/run_meta/HTML 에 char_budget_estimate 노출.

### 6-C. Schema shape (Stage A + Stage B) (7)
9. `test_world_brief_schema_has_required_top_level_keys` — Stage A: schema_version / bundle_id / world_background_brief / confidence_band 4 키.
10. `test_world_brief_required_subfields` — `world_background_brief` 가 era_and_time_period / geographic_cultural_grounding / technology_and_material_baseline / social_economic_visual_tone / genre_mood_constraints / background_relevant_do_not_assume / irrelevant_or_do_not_pass_down / evidence_refs 8 subfield 필수.
11. `test_minimal_brief_schema_has_required_top_level_keys` — Stage B: schema_version / bundle_id / world_brief_ref / world_hints_for_background / place_identity / continuity_groups / essential_spatial_relations / visual_anchors / state_variations / generation_notes / confidence_band 11 키.
12. `test_minimal_brief_does_not_embed_full_world_context` — Stage B schema 에 `world_context_brief` 또는 `world_background_brief` 전체 dict embedding 0. checker 가 이를 enforce.
13. `test_minimal_brief_evidence_ref_required_fields` — evidence_refs entry 의 source_ref / quote / confidence 필수, char_start/end optional. 두 schema 공통.
14. `test_schemas_have_no_count_json_key` — 두 schema 모두 output JSON key 이름 `count` 0 (W0f IMPORTANT 2 명확화: source quote 본문 내 "두 개" / 숫자 등은 허용).
15. `test_minimal_brief_visual_anchor_uses_base_plate_role_not_must_appear` — visual_anchors item 에 `base_plate_role` enum (identity_anchor / optional / avoid) 필수, `must_appear_in_base_plate` 필드 0 (W0f IMPORTANT 3 완화).

### 6-D. Deterministic checker (provenance & shape only + cap + leakage guard) (6)
16. `test_checker_validates_quote_exact_containment_in_source_text` — synthetic brief (Stage A 또는 Stage B) 의 quote 가 source text 에 없으면 fail.
17. `test_checker_augments_quote_occurrence_positions` — quote 존재 시 char_start/end 가 validation_report.augmented_occurrences 기록.
18. `test_checker_rejects_topology_or_count_or_embedded_world` — brief JSON 에 topology 확정 필드 / `count` JSON 키 / Stage B 내 world_background_brief 전체 embedding 등장 시 fail (필드 이름 enum 검사).
19. `test_checker_validates_two_schemas_independently` — Stage A 와 Stage B 가 각각 evidence_refs / source_ref / quote / confidence 검사. 한 stage fail 다른 stage 통과 가능.
20. `test_checker_enforces_minimality_caps` — array 길이 / 문장 길이 cap 초과 시 fail (W0f BLOCKING 1). 예: continuity_groups 4개 / world_hints item 200chars / base_plate_prompt_brief 1500chars 등 synthetic over-cap brief 가 모두 fail. 정확히 cap 경계 (length=cap_value) 는 PASS.
21. `test_checker_enforces_stage_b_input_contract` (W0g BLOCKING — W0f substring 비교 폐기). Stage B input object / prompt builder 의 **구조** 검사만. content/substring 비교 X. 구체:
   - Stage B input dict 의 top-level key 에 `world_background_brief` 없음.
   - Stage B input dict 의 top-level key 에 `irrelevant_or_do_not_pass_down` 없음.
   - Stage B input 의 world 관련 키 = `{world_brief_ref, world_hints_for_background}` 정확히 2개만 (그 외 world 계열 키 등장 시 fail).
   - Stage B prompt builder function 의 signature (`ast.FunctionDef.args`) 에 `world_background_brief` / `irrelevant_or_do_not_pass_down` parameter 0.
   - 의미적 plot/lore 가 hint 에 다시 들어가는 경우는 deterministic 검사 X (W2 LLM/human review).

### 6-E. Prompt / CLI / production / output (2)
22. `test_prompts_separate_world_and_spatial_and_spatial_builder_excludes_full_stage_a` — `prompt/world_system.txt` + `prompt/spatial_system.txt` + `prompt/world_user.txt` + `prompt/spatial_user.txt` 4 파일 모두 존재 (file presence). spatial prompt **builder function 의 signature 검사**: `world_background_brief` / `irrelevant_or_do_not_pass_down` parameter 0; `world_hints_for_background` parameter 만 받음. spatial_user.txt 의 본문 content grep 안 함 (W0g BLOCKING — 글자 검사 회귀 방지). prompt 의 의미 표시는 builder source 에 명시 키워드를 박을지 여부는 W1 구현 자유, test 가 강제 X.
23. `test_cli_generate_default_off_and_no_network_no_db_write_and_outputs_13` — `--generate` 미지정 시 generate=False; `--dry-run` + `--generate` 동시 지정 시 generate=False; LLM 호출 0; `git diff --quiet HEAD -- backend/app` empty; session.add/commit/flush/delete + INSERT/UPDATE/DELETE 0; dry-run 시 urllib/socket/requests/httpx 호출 0; 13 산출 파일 모두 존재; HTML 에 "minimal" / "why minimal" / "2-stage" / "world brief" 표현 + diagnostic flag 가시화.

### 6-F. No-truncate / no-auto-shorten / verbatim source guards (W1b+W1c 사용자 standing rule, 5 test)
24. `test_checker_preserves_over_cap_brief_content_verbatim` — over-cap world hint 가 checker 통과 후 brief dict 의 content 가 변경 0. cap_exceeded fail 기록 확인.
25. `test_source_bundle_preserves_long_input_text_verbatim` — synthetic 50K chars planning + 30K chars episode 가 source_bundle.text 에 truncate 없이 그대로. char_count 정확 일치.
26. `test_script_has_no_truncation_logic_for_brief_or_source` — script AST 의 모든 `ast.Subscript(slice=ast.Slice(...))` 의 value source 가 brief content field name 을 substring 포함하면 fail. script body 에 `textwrap.shorten` / `.truncate(` / `[:cap]` / `[:MAX_*]` / `[:max_*]` 패턴 0.
27. `test_source_loaders_do_not_strip_or_normalize_text` (W1c BLOCKING) — DB loaders + `_make_source_item` + `build_source_bundle` 함수 AST 안 `.strip` / `.lstrip` / `.rstrip` 호출 0. `unicodedata.normalize` 호출 0 + `import unicodedata` 0 (entire script).
28. `test_source_bundle_preserves_leading_trailing_whitespace_verbatim` (W1c BLOCKING) — `build_source_bundle` 에 leading/trailing whitespace/newline/tab 포함한 planning_text / episode_text 입력 시 `source_bundle.text` 가 정확히 그대로 보존. `char_count == len(original_text)`.

backend pytest 전체 회귀 0 강제. 6 그룹 28 test (W1b 26 → W1c 28, +2 verbatim source guards).

---

## 7. Non-scope (W1+ 별도 wave)

- Topology graph 생성 (SetTopologyGraph / StructuralStateModel).
- Background generation unit 결정.
- Image generation / chain_bg / floor plan.
- LLM 결과의 의미 second-guessing (deterministic 분류 보강).
- Production prompt/code 수정.
- Lexicon matcher / regex classifier / 글자 boundary / 조사 분리 / particle list.
- 기존 `background_topology_planner_experiment` W1~W1e 산출/diff 의 SOT 승격.
- runtime forbidden-word / brief content scan (W0b BLOCKING 1).
- selected_shots text substring filter (W0b BLOCKING 2).
- door/window/furniture/object inventory + count 강제 (W0e 사용자 지적). LLM 이 source 가 명확히 그렇게 말할 때만 brief 안에 적도록 prompt 지시; deterministic 강제 X.
- 9-category 또는 8-category 세분화 분류 (W0c/W0d 폐기, W0e MinimalSpatialBrief 단일 산출).

---

## 8. LLM 모델 (candidate only — W2 entry 전 재검증, IMPORTANT 2)

W1 dry-run 단계에서는 model identifier 만 placeholder. 실제 호출 0.

| 후보 | candidate 강점 (W2 generate 전 재검증 필요) |
|---|---|
| `gemini-3.1-pro-preview` (candidate only) | CLAUDE.md 명시 — 공간/배경/소울라이드/물리적 존재 판별 우수 |
| `gpt-5.5` (base/pro) (candidate only) | CLAUDE.md 명시 — 텍스트 분석 주력, JSON mode 안정적 |

W1 default model = `"model_for_future_generate"` placeholder. W2 plan 작성 시 실제 사용 가능한 모델 + structured output 지원 + token context 한도 확인 후 default 확정.

---

## 9. Wave 구조

| wave | 산출 | scope |
|---|---|---|
| W0 (본 plan) | plan.md 만 | 설계 합의, 코드 0 |
| W0.5 (Codex W0b APPROVED 후) | cleanup diff | 기존 `background_topology_planner_experiment` 의 aborted W1e literal-boundary 코드/test 변경 revert. approved W1b/W1d snapshot 산출 + plan §8-G deprecated 마킹은 보존. backend/app 변경 0. §13 참조. |
| W1 (W0.5 후) | script + tests + dry-run 산출 13 파일 (W0e 2-stage) | source bundle + 두 schema + 두 prompt + checker plan, `--generate` off. 회귀 0. model placeholder. |
| W2a (Codex 2026-05-24 사용자 지시) | Evidence Filter stage 추가 (Filter → World → Spatial 3-stage) — 산출 18 파일 (valid pack) 또는 19 파일 (invalid pack quarantined) | 신규 `BackgroundEvidencePack` schema/prompt/checker. raw `SourceBundle` 은 Evidence Filter 만 한 번 읽고, downstream world/spatial prompt builder 는 filtered evidence_pack 만 받음 (시그니처에서 `bundle` 제거 — test enforce). `--generate` on 시 `gemini-3.5-flash` (default) 또는 `--model` override 로 LLM 1회 호출, retry 1, fail-closed (api unreachable / non-JSON / validation fail). 신규 test 그룹 6-G 23 + 갱신 6-E 2. backend/tests/scripts 회귀 0. CLAUDE.md "절대 자르지 마라" 준수. |
| W2a-b (Codex W2a NEEDS_REVISION 후 narrow patch) | route_evidence_filter_result helper + prompt 강화 + plan SOT 갱신 | BLOCKING 1: "LLM call succeeded" 와 "EvidencePack valid" 분리 — `route_evidence_filter_result()` 가 validation_failed 시 invalid pack 을 `background_evidence_pack_quarantined.json` 으로 quarantine, downstream prompt 는 placeholder 받음, `main()` exit code 1, `filter_validation_report.run_status` 명시 (succeeded / validation_failed / generate_failed). BLOCKING 2: evidence_filter system + user prompt 본문에 `confidence_band` enum `trusted/plausible/weak/unknown` 명시 + "do not use high / medium / low". IMPORTANT 1: world/spatial system prompt 본문 "SourceBundle" → "filtered BackgroundEvidencePack" 정정 (시그니처는 W2a 에서 이미 변경). IMPORTANT 2: 본 §9/§10 갱신. MINOR: test 이름 19→18 일관성 + Evidence Filter static guard 주석에 "quote containment find 는 허용" 명시. 신규 test 그룹 6-H 8. |
| W2a-c (Codex W2a-b NEEDS_REVISION 후 narrow patch) | run_meta + HTML status 정직 + PLAN_VERSION bump | BLOCKING 1: `build_run_meta()` 시그니처에 run_status / exit_code / filter_validation_passed (tri-state None for dry-run) / generate_status / filter_failed_checks / quarantine_output 추가. outputs 목록에 quarantine 조건부 포함. HTML 상단 배지 상태별 (dry_run/succeeded/validation_failed/generate_failed), validation_failed 는 빨간 경고 박스. placeholder 문구 reason 기반 분기. IMPORTANT 1: PLAN_VERSION `bsx_w1` → `bsx_w2a_b`, HTML `<title>` "W2a-b (Evidence Filter gateway)", CLI description 3-stage 명시. MINOR: module docstring 갱신. 신규 test 그룹 6-I 7. |
| W2a-d (Codex W2a-c APPROVED_FOR_DIAGNOSTIC + 사용자 명시 지시 진입) | LLM correction retry (quote_not_in_source 한정) | `build_prompt_evidence_filter_correction_system/user(*, spec, bundle, schema, invalid_pack, failed_checks)` 신설 — full SourceBundle inline, source 자르지 않음. `route_evidence_filter_result()` 에 optional `correction_call` + `model` — initial validation_failed + `quote_not_in_source` 포함 시 정확히 1회 correction. promote / quarantine / exception 분기. correction_attempted/status/model/failed_checks/evidence_filter_attempts (initial+correction) 기록. HTML badge "succeeded (filter validated after correction)" 녹색. `_default_evidence_filter_correction_llm_call` (litellm gemini/*, raise on fail). 신규 test 그룹 6-J 13. **첫 valid EvidencePack 산출 성공** (run `20260524_1712_d575f7`, 8 items, augmented_quote_count=8). |
| W2a-e (Codex W2a-d NEEDS_REVISION_BEFORE_W2B 후 narrow patch) | run_meta filter_failed_checks 정직 + SOT/version 동기화 | BLOCKING 1: `run_meta.filter_failed_checks` = final validation 기준 (성공 after correction 시 `[]`). 새 키 `initial_failed_checks` 분리 — pre-correction 실패 audit. `correction_failed_checks` = post-correction 실패만. HTML 에 "initial quote failures corrected: N" green 박스. IMPORTANT 1: PLAN_VERSION → `bsx_w2a_d`. HTML `<title>` "W2a-d (Evidence Filter gateway + correction retry)". CLI description correction 명시. MINOR: module docstring 갱신. 신규 test 6-I 그룹 +5 (총 84 test). |
| W2a-f (Codex W2a-e APPROVED 후 사용자 EvidencePack refinement 지시 — W2b 진입 전 품질 정렬) | EvidencePack schema 개편 + HTML downstream-first | 사용자 지적: (1) HTML 이 raw 79 sources 를 먼저 보여줘서 "이걸 다 입력하나" 오해, (2) `pass_down_hint` 가 T2I prompt 처럼 행동, (3) master/global identity 와 state variation 섞임, (4) world default 약함. 변경: `pass_down_hint`/`use_for`/`why_background_relevant` 폐기 → `background_fact`/`role`/`applies_to`/`why_keep` 도입. role enum 6 (global_identity/world_default/layout_feel/state_variation/avoid_default/gap), applies_to enum 5 (master_background/specific_state_only/shot_specific/avoid/unknown). PLAN_VERSION → `bsx_evidence_pack_w2a_f`. evidence_filter system/user prompt 재작성 (image command 금지 + master/state 분리 + generic enum 의미 설명). correction prompt 도 새 schema. downstream world/spatial prompt builder 도 새 키로. HTML §1 = "Refined downstream evidence pack" (role 별 grouping, downstream input 명시). raw SourceBundle 은 §99 `<details>` 접힘. 신규 6-K group test 9. 절대 조건: deterministic checker 가 role 의미 second-guess 금지 (enum membership 만). scenario-specific sample 단어 schema/prompt/test 에 0. | §9 W2a-f, script `build_background_evidence_pack_schema`/`build_prompt_evidence_filter_system`/`build_prompt_evidence_filter_user`/`run_evidence_filter_validation`/`render_html`, test 6-K |
| W2b (별도, 미진입) | `--generate` 에서 world + spatial stage 도 실제 호출 (chained on Evidence Filter) | Stage A 호출 → Stage B 호출 (sequential). char_budget fail-closed. 10 downstream checker 실행 (cap + structural input_contract 포함). |
| W3 | sample fixture 외 추가 fixture 확장 | spec swap test, 다른 location 으로 generic 검증. |
| W4+ | topology candidate generation (별도 plan) | 본 wave 와 분리. |

---

## 10. §10 결정 (W0b/W0c/W0e 닫음)

| # | 의논 항목 | Codex / 사용자 결정 | 반영 위치 |
|---|---|---|---|
| 1 | LLM default 모델 | W1 미확정 placeholder, W2 전 재검증 | §4, §8 |
| 2 | token 한도 vs 자르기 금지 양립 | W1 truncate/drop/chunk 금지, W2 generate 시 fail-closed | §1, §5, §11 |
| 3 | category 세분화 (8/9 candidate kind) | **폐기 (W0e)** — MinimalSpatialBrief 단일 객체 | §2 |
| 4 | char_start/char_end optional vs required | optional 유지, checker 가 quote occurrence 보강 (augmentation) | §2, §3, §6 |
| 5 | no_pattern_reasoning forbidden word checker | **제거** (W0b BLOCKING 1). prompt 가 LLM 지시, 정적 가드만 enforce | §0, §3, §6 |
| 6 | W1 vs W2 분리 vs 합치기 | 분리 유지, W1 dry-run only | §9 |
| 7 | LLM hyperparams (temperature / max_tokens / retry) | W1 placeholder, W2 plan 에서 CLI flag 결정 | §4, §8 |
| 8 | 추가 fixture (W3) 사전 후보 | W1 필수 아님, W2 generate 전에 후보 정해도 됨 | §9 |
| 9 | 기존 background_topology_planner_experiment artifact | approved snapshot 보존, aborted W1e diff 는 W0.5 cleanup | §13 |
| 10 | W1 HTML cockpit 구조 | minimal brief: source/schema/prompt/why minimal/planned validation 충분 | §5 |
| 11 (W0e) | door/window/furniture count 강제 | **금지** — schema 에 `count` field 0, prompt "do not inventory objects or count" | §2, §3, §6 |
| 12 (W0e 정정) | world context 처리 | **2-stage LLM flow** — Stage A `BackgroundWorldBrief` 가 압축/필터링, Stage B `MinimalSpatialBrief` 가 짧은 world_hints_for_background 만 참조. embedding 금지. | §2-A, §2-B, §3, §5, §6 |
| 13 (W0f) | minimality 보장 | array 길이 + 문장 길이 cap 명시 (continuity_groups max 3, visual_anchors max 5 등) + checker 가 enforce. shape/minimality 검증, 의미 분류 아님. | §2-B, §3 |
| 14 (W0f) | base_plate anchor 강도 | `must_appear_in_base_plate: true` 폐기 → `base_plate_role: identity_anchor | optional | avoid` 완화. base plate 가 모든 anchor 의 inventory 아님. | §2-B |
| 15 (W0f) | `count` 금지 범위 | JSON output key 이름 `count` 만 거부. source quote 본문의 숫자/표현 ("두 개" 등) 은 provenance 데이터, 허용. | §3, §6 |
| 16 (W0f) | Stage A → B leakage W1 template 보장 | spatial prompt template 에 full Stage A dict / irrelevant list embed 0. test 가 enforce. W2 cross-check 외 W1 dry-run 단에서도 강제. | §6 22, §3 10 |
| 17 (W0f) | W0c skeleton 처리 | patch 아니라 **폐기 + 재작성**. W0g APPROVED 후 §14 cleanup 으로 삭제 → W1 새 script. | §14 |
| 18 (W0g) | Stage B leakage enforcement 방식 | substring/content 비교 폐기 → **structural input contract** 만. Stage B input dict 의 key 부재 + prompt builder signature parameter 부재. 의미적 leak 은 deterministic 검사 X (W2 LLM/human review). | §3 #10, §6-D-21, §6-E-22 |
| 19 (W0g) | identity_anchor 의미 | "반드시 base plate 에 흔적" → "base plate should be informed by this anchor" 로 완화. 직접 inventory 회귀 방지. | §2-B |
| 20 (W1b) | cap 초과 처리 방식 (사용자 추가 standing rule) | **truncate / slice / drop / chunk / auto-shorten 모두 금지**. checker 가 cap 초과를 validation failure 로만 기록. 원문 보존 verbatim. checker 함수 / source bundle builder / prompt builder 어느 곳도 brief content 또는 source.text 를 자르지 않음. test 6-F-24/25/26 가 enforce. | §3 #9, §6-F, validation_report `cap_exceeded:*` |
| 21 (W1c) | source text "verbatim 보존" 의 실제 정의 | DB loader `.strip()` / `.lstrip()` / `.rstrip()` 금지 + `_make_source_item` 의 `unicodedata.normalize` 제거. `None` 만 `""` 치환, 그 외 모든 문자열 (leading/trailing whitespace/newline/tab 포함) 그대로. 6-F-26 audit 에 strip/normalize/drop 포함. 신규 test 6-F-27 (loader AST `.strip` 부재) + 6-F-28 (whitespace 보존). | §3 #9, §6-F-27/28 |
| 22 (W2a) | 79 raw sources 가 최종 배경 프롬프트 입력이 아님 — Evidence Filter 신설 | 신규 stage Gateway: LLM 이 `SourceBundle` 전체 1회 읽고 `BackgroundEvidencePack` 산출. downstream world/spatial prompt builder 시그니처에서 `bundle` 제거 (test enforce), evidence_pack 만 받음. caps: evidence_items ≤ 40 / why ≤ 180 / pass_down_hint ≤ 160 / coverage_notes ≤ 8 / rejected_or_irrelevant_summary ≤ 8. use_for enum 5종 (world_context / place_identity / spatial_scale_or_layout_feel / state_variation / visual_default_or_avoid). forbidden keys: topology fields + count / door_count / window_count / furniture_count / object_count / inventory. CLAUDE.md "절대 자르지 마라" 준수 (cap 초과 = FAIL, 원문 verbatim). 신규 산출 6 파일 (filter_schema + prompts 2 + pack + filter_validation_report + index.html 업데이트). | §9 W2a, script `experiment_background_semantic_extractor.py`, test 6-G |
| 23 (W2a-b) | "API 성공 ≠ 유효한 pack" 분리 + prompt enum 강화 | `route_evidence_filter_result()` 가 validation 결과 따라 분기: passed=True → downstream 에 pack 전달 / passed=False → quarantine (`background_evidence_pack_quarantined.json`) + downstream placeholder + `main()` exit 1 + `filter_validation_report.run_status="validation_failed"`. evidence_filter system/user prompt 에 `trusted/plausible/weak/unknown` 명시 + "do not use high / medium / low". world/spatial system prompt 본문도 "filtered BackgroundEvidencePack" 으로 일관 (시그니처는 W2a 에서 이미 변경됨). | §9 W2a-b, script `route_evidence_filter_result`, test 6-H |
| 24 (W2a-c) | run_meta + HTML 이 실제 run 상태 정직하게 노출 | `build_run_meta()` 새 키: run_status / exit_code / filter_validation_passed (tri-state None for dry-run) / generate_status / filter_failed_checks / quarantine_output. outputs 조건부 quarantine. HTML badge 4 분기 (dry_run/succeeded/validation_failed/generate_failed), validation_failed 첫 화면 빨간 경고 박스. placeholder pack 문구도 reason 기반 분기. PLAN_VERSION → `bsx_w2a_b`, HTML `<title>` 갱신, CLI 3-stage 명시. | §9 W2a-c, script `build_run_meta`/`render_html`/`main`, test 6-I |
| 25 (W2a-d) | quote_not_in_source 실패에만 1회 LLM correction retry | deterministic code 가 quote 의미적 수정 / fuzzy match 금지. correction prompt 는 full SourceBundle + invalid pack + failed_checks 전달 (자르지 않음). `route_evidence_filter_result()` 에 optional `correction_call` — `quote_not_in_source` 있을 때만 정확히 1회. promote on success / quarantine corrected pack on validation_failed / quarantine original on LLM exception. enum/cap/schema 실패만이면 보수적 fail-closed (correction skip). evidence_filter_attempts (initial + correction) 카운트. | §9 W2a-d, script `route_evidence_filter_result`/`build_prompt_evidence_filter_correction_*`/`_default_evidence_filter_correction_llm_call`, test 6-J 13 |
| 26 (W2a-e) | run_meta `filter_failed_checks` 의미 정직화 | success after correction 시 top-level `filter_failed_checks` = `[]` (FINAL 상태). pre-correction 실패는 별도 `initial_failed_checks` 로 보존 (audit). correction 후에도 실패 시 top-level 은 LATEST (post-correction) 실패, `initial_failed_checks` 는 pre-correction. HTML 에 "initial quote failures corrected: N" green 박스. PLAN_VERSION → `bsx_w2a_d`, HTML `<title>` "W2a-d (Evidence Filter gateway + correction retry)". CLI description correction 명시. module docstring 갱신. | §9 W2a-e, script `build_run_meta`/`render_html`, test 6-I +5 |
| 27 (W2a-f) | EvidencePack refinement — downstream-friendly schema + HTML downstream-first | 사용자 W2b 진입 전 품질 정렬 지시: pass_down_hint 가 image command 처럼 동작 + master/state 섞임 + world default 약함 + HTML 이 raw 79 sources 를 먼저 보여줘 오해. 변경: evidence_item required = evidence_id/source_ref/quote/background_fact/role/applies_to/why_keep/confidence_band. role enum 6 (global_identity/world_default/layout_feel/state_variation/avoid_default/gap), applies_to enum 5 (master_background/specific_state_only/shot_specific/avoid/unknown). pass_down_hint/use_for/why_background_relevant 모두 forbidden key 등록. prompt: image command 금지 + master/state 분리 + generic enum 의미 설명 (sample 단어 0). HTML §1 = Refined downstream evidence pack (role 별 grouping), raw SourceBundle 은 §99 `<details>` 접힘. deterministic checker = role 의미 second-guess 0 (enum membership 만). | §9 W2a-f, script `build_background_evidence_pack_schema`/`build_prompt_evidence_filter_system`/`render_html`, test 6-K |

---

## 11. Risk

- **LLM hallucinated quote**: prompt 에 "quote 는 source text 그대로 복사" 강제 + checker quote_exact_containment fail 시 LLM 재호출. provenance 검증으로 hallucination 차단.
- **Schema drift**: LLM 이 schema 외 필드 emit. checker no_forbidden_fields (topology + count) + schema_required_fields 로 enforce + prompt 에 strict mode 명시.
- **Token cost**: dry-run default 라 cost 0. generate wave 만 cost 발생, 한 번에 1 run 이라 통제 가능.
- **모델 변경 시 SOT drift**: model identifier + temperature 를 run_meta 에 박아 결정성 유지. 단 W1 은 placeholder.
- **사용자 standing rule 회귀 위험 (글자 패턴 인지)**: prompt + 정적 가드 (re import 부재 + lexicon constant 부재 + classifier function 부재 + checker 본문 content scan 부재) 4겹 방어. checker 자체는 패턴 검사 X.
- **token context 초과 risk (CLAUDE.md "절대 자르지 마라" + 큰 source bundle)**: W2 fail-closed 로 cost 0. 별도 plan 에서 model context 큰 모델로 교체 또는 source pre-summarization 결정.
- **W1e aborted diff 잔존 risk**: W0.5 cleanup wave 가 명시적으로 revert (완료, 49+326 PASS).
- **Over-extraction risk (W0e 새)**: 세분화 schema 가 T2I/I2I 에 도움 안 되거나 오히려 해 — minimal brief 로 축소해 mitigate. 단 minimal 이 정보 부족 시 W2 결과 보고 step-by-step 보강 (사용자 지적 "단순한 방식으로 시작하고, 이후 조금씩 개선").
- **World over-spill risk (W0e 정정 후)**: world context 를 brief 에 그대로 끼우면 T2I 가 plot/lore 시각화로 틀어짐. 2-stage flow + Stage A 의 `irrelevant_or_do_not_pass_down` + Stage B 의 short hints reference + checker 의 `no_embedded_world` enforcement 4겹 방어.
- **Default visual assumption risk (T2I 럭셔리/서구/미래/판타지 default)**: Stage A 의 `background_relevant_do_not_assume` list + Stage B 의 generation_notes.avoid_over_specification 로 mitigate. world hints 가 짧아도 default 회피 안전장치 명시.

---

## 12. 진행

W0g (본 patch) = substring leakage check 폐기 → structural input contract / identity_anchor 완화 / checker count 표기 정리 완료. 다음 = Codex W0g 재리뷰. APPROVED_FOR_W1 시 §14 W0c skeleton cleanup (script 삭제) → W1 새 script + 23 test 작성 (W0e/W0f/W0g 2-stage contract 기준). 코드 자체는 본 patch 에서 수정 0, dry-run 산출 0.

---

## 13. W0.5 cleanup wave (Codex BLOCKING 3)

### 13-A. Scope

`background_topology_planner_experiment` 의 aborted W1e literal-boundary diff 를 git checkout 으로 revert. approved W1b/W1d snapshot 산출 + plan §8-G deprecated 마킹 + W1c/W1d run dir 은 보존.

### 13-B. Revert 대상 (W1e aborted 만 — W0c BLOCKING 1 명확화)

W1d 는 readiness state_model + metric key rename + plan §8-F 만 추가한 wave. W1e 가 별도로 BLOCKING 0 (shot location filter via substring) + IMPORTANT A (literal-boundary helper) + IMPORTANT B (display dedup, 미완) + IMPORTANT C (시신 reclass, 미완) 시도하다 중단. 본 §13 cleanup 는 **W1e 변경 전부**가 revert 대상이다 — W1d 는 손대지 않음.

1. `backend/scripts/experiment_background_topology_planner.py` — W1e aborted patch 들 모두 revert:
   - `_HANGUL_RE`, `_KOREAN_PARTICLES`, `_is_alnum`, `_is_hangul`, `_is_valid_literal_match` 신설 → 제거
   - `_iter_term_positions` 의 boundary guard 호출 → 원복
   - `main()` 의 `load_selected_shots(..., loc_short_ids=[spec.location_short_id])` 추가 → 원복 (이건 W1e BLOCKING 0 시도였고 substring 기반 old loader 호출 — 새 semantic extractor 원칙과 충돌, 따라서 revert)
   - `SAMPLE_FIXTURE_L05_STATE_TERMS` 의 `시신` reclass → 원복
2. `backend/tests/scripts/test_experiment_background_topology_planner.py` — 4-K 그룹 9 test (W1e literal-boundary tests) 제거.
3. `scripts_output/background_topology_planner_experiment/plan.md` — §8-G deprecated 마킹 + 변경은 보존 (이력 남김), 단 §8-G 본문 변경 없음.

### 13-C. 보존 대상 (W1d approved + W1b approved snapshot)

1. W1b approved run dir `20260524_1333_02f040/` — APPROVED snapshot.
2. W1c run dirs `20260524_1355_*_w1c/` — W1c review cockpit 산출.
3. W1d run dirs `20260524_1401_*_w1c/` — W1d review cockpit 산출 (W1d 는 별도 `_w1d` 서픽스 없이 같은 `_w1c` 사용 — Codex W1d 권고 그대로).
4. W1d 코드/test/plan diff:
   - `_evaluate_readiness` state_model conditional downgrade (W1d IMPORTANT 1)
   - `DEFAULT_READINESS_THRESHOLDS` 의 `boundary_rows` / `door_window_rows` key rename (W1d IMPORTANT 2)
   - topo_obs key rename
   - 4-J 그룹 3 test
   - plan §8-F (W1d narrow patch erratum)
5. `scripts_output/background_topology_planner_experiment/plan.md` 의 §1~§8-F 본문.

새 semantic extractor (`background_semantic_extractor_experiment`) 의 selected-shot scope 는 **새 script** 에서 `visible_entities_json` JSON parse + `short_id == spec.location_short_id` exact equality 로 새로 구현. 기존 `experiment_background_topology_planner.py` 의 `load_selected_shots` 는 그대로 두되 본 wave SOT 가 아님.

### 13-D. 검증

- cleanup 후 `git diff --stat backend/app backend/alembic` empty 유지.
- cleanup 후 `backend/tests/scripts` regression 0 (W1d 시점과 동일).
- cleanup 후 `background_topology_planner_experiment` 의 plan §8-G 가 "DEPRECATED" 마킹 그대로 (history 보존).
- cleanup 자체는 별도 commit 없음 — W1 진입 전 working tree 정리.

### 13-E. 진입 조건

W0b APPROVED → W0.5 cleanup 실행 → W1 진입 (W0.5 = `background_topology_planner_experiment` 의 W1e literal-boundary aborted diff 정리. 완료 — 49+326 PASS, app diff empty).

---

## 14. W0c skeleton cleanup wave (Codex W0e BLOCKING 2)

### 14-A. Scope

`backend/scripts/experiment_background_semantic_extractor.py` 는 W0c 8-category schema 기반 skeleton 으로 작성됨. Codex W0e BLOCKING 2 명시: "patch 하지 말고 사실상 폐기/재작성. W1 script/test 는 W0e/W0f 2-stage contract 기준으로 새로 작성하는 게 안전".

### 14-B. Delete 대상

1. `backend/scripts/experiment_background_semantic_extractor.py` — 전체 삭제. W0c `EXTRACTION_CATEGORIES`, `place_set_candidates` 등 stale references 가 W0e/W0f 2-stage 와 호환 안 됨.
2. (test 파일 없음, 작성 전이라 N/A.)

### 14-C. 보존 대상

- `scripts_output/background_semantic_extractor_experiment/plan.md` (W0f 본 문서).
- 다른 `background_topology_planner_experiment/` 산출/test (모두 W0.5 cleanup 후 stable).

### 14-D. 검증

- delete 후 `git status` 에서 `?? backend/scripts/experiment_background_semantic_extractor.py` 사라짐.
- `git diff --stat backend/app backend/alembic` empty 유지.
- `PYTHONPATH=backend .venv/bin/python -m pytest backend/tests/scripts -q` regression 0 유지 (target 49 pass — 본 script 영향 없음).

### 14-E. 진입 조건

W0g APPROVED_FOR_W1 → §14 cleanup 실행 (script 삭제) → W1 진입 (새 script + 23 test 작성, W0e/W0f/W0g 2-stage contract 기준).
